1use std::cmp;
18use std::collections::HashMap;
19use std::rc::Rc;
20use std::sync::Arc;
21
22use clap::ValueEnum;
23use itertools::Itertools as _;
24use jj_lib::backend;
25use jj_lib::backend::BackendResult;
26use jj_lib::backend::CommitId;
27use jj_lib::config::ConfigValue;
28use jj_lib::store::Store;
29
30use crate::commit_templater::CommitRef;
31
32#[derive(Clone, Debug)]
33pub struct RefListItem {
34 pub primary: Rc<CommitRef>,
36 pub tracked: Vec<Rc<CommitRef>>,
38}
39
40#[derive(Copy, Clone, PartialEq, Debug, ValueEnum)]
42pub enum SortKey {
43 Name,
44 #[value(name = "name-")]
45 NameDesc,
46 AuthorName,
47 #[value(name = "author-name-")]
48 AuthorNameDesc,
49 AuthorEmail,
50 #[value(name = "author-email-")]
51 AuthorEmailDesc,
52 AuthorDate,
53 #[value(name = "author-date-")]
54 AuthorDateDesc,
55 CommitterName,
56 #[value(name = "committer-name-")]
57 CommitterNameDesc,
58 CommitterEmail,
59 #[value(name = "committer-email-")]
60 CommitterEmailDesc,
61 CommitterDate,
62 #[value(name = "committer-date-")]
63 CommitterDateDesc,
64}
65
66impl SortKey {
67 fn is_commit_dependant(&self) -> bool {
68 match self {
69 Self::Name | Self::NameDesc => false,
70 Self::AuthorName
71 | Self::AuthorNameDesc
72 | Self::AuthorEmail
73 | Self::AuthorEmailDesc
74 | Self::AuthorDate
75 | Self::AuthorDateDesc
76 | Self::CommitterName
77 | Self::CommitterNameDesc
78 | Self::CommitterEmail
79 | Self::CommitterEmailDesc
80 | Self::CommitterDate
81 | Self::CommitterDateDesc => true,
82 }
83 }
84}
85
86pub fn parse_sort_keys(value: ConfigValue) -> Result<Vec<SortKey>, String> {
87 if let Some(array) = value.as_array() {
88 array
89 .iter()
90 .map(|item| {
91 item.as_str()
92 .ok_or("Expected sort key as a string".to_owned())
93 .and_then(|key| SortKey::from_str(key, false))
94 })
95 .try_collect()
96 } else {
97 Err("Expected an array of sort keys as strings".to_owned())
98 }
99}
100
101pub fn sort(
106 store: &Arc<Store>,
107 items: &mut [RefListItem],
108 sort_keys: &[SortKey],
109) -> BackendResult<()> {
110 let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();
111 if sort_keys.iter().any(|key| key.is_commit_dependant()) {
112 commits = items
113 .iter()
114 .filter_map(|item| item.primary.target().added_ids().next())
115 .map(|commit_id| {
116 store
117 .get_commit(commit_id)
118 .map(|commit| (commit_id.clone(), commit.store_commit().clone()))
119 })
120 .try_collect()?;
121 }
122 sort_inner(items, sort_keys, &commits);
123 Ok(())
124}
125
126fn sort_inner(
127 items: &mut [RefListItem],
128 sort_keys: &[SortKey],
129 commits: &HashMap<CommitId, Arc<backend::Commit>>,
130) {
131 let to_commit = |item: &RefListItem| {
132 let id = item.primary.target().added_ids().next()?;
133 commits.get(id)
134 };
135
136 for sort_key in sort_keys
139 .iter()
140 .rev()
141 .skip_while(|key| *key == &SortKey::Name)
142 {
143 match sort_key {
144 SortKey::Name => {
145 items.sort_by_key(|item| {
146 (
147 item.primary.name().to_owned(),
148 item.primary.remote_name().map(|name| name.to_owned()),
149 )
150 });
151 }
152 SortKey::NameDesc => {
153 items.sort_by_key(|item| {
154 cmp::Reverse((
155 item.primary.name().to_owned(),
156 item.primary.remote_name().map(|name| name.to_owned()),
157 ))
158 });
159 }
160 SortKey::AuthorName => {
161 items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.name.as_str()));
162 }
163 SortKey::AuthorNameDesc => {
164 items.sort_by_key(|item| {
165 cmp::Reverse(to_commit(item).map(|commit| commit.author.name.as_str()))
166 });
167 }
168 SortKey::AuthorEmail => {
169 items
170 .sort_by_key(|item| to_commit(item).map(|commit| commit.author.email.as_str()));
171 }
172 SortKey::AuthorEmailDesc => {
173 items.sort_by_key(|item| {
174 cmp::Reverse(to_commit(item).map(|commit| commit.author.email.as_str()))
175 });
176 }
177 SortKey::AuthorDate => {
178 items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.timestamp));
179 }
180 SortKey::AuthorDateDesc => {
181 items.sort_by_key(|item| {
182 cmp::Reverse(to_commit(item).map(|commit| commit.author.timestamp))
183 });
184 }
185 SortKey::CommitterName => {
186 items.sort_by_key(|item| {
187 to_commit(item).map(|commit| commit.committer.name.as_str())
188 });
189 }
190 SortKey::CommitterNameDesc => {
191 items.sort_by_key(|item| {
192 cmp::Reverse(to_commit(item).map(|commit| commit.committer.name.as_str()))
193 });
194 }
195 SortKey::CommitterEmail => {
196 items.sort_by_key(|item| {
197 to_commit(item).map(|commit| commit.committer.email.as_str())
198 });
199 }
200 SortKey::CommitterEmailDesc => {
201 items.sort_by_key(|item| {
202 cmp::Reverse(to_commit(item).map(|commit| commit.committer.email.as_str()))
203 });
204 }
205 SortKey::CommitterDate => {
206 items.sort_by_key(|item| to_commit(item).map(|commit| commit.committer.timestamp));
207 }
208 SortKey::CommitterDateDesc => {
209 items.sort_by_key(|item| {
210 cmp::Reverse(to_commit(item).map(|commit| commit.committer.timestamp))
211 });
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use jj_lib::backend::ChangeId;
220 use jj_lib::backend::MillisSinceEpoch;
221 use jj_lib::backend::Signature;
222 use jj_lib::backend::Timestamp;
223 use jj_lib::backend::TreeId;
224 use jj_lib::merge::Merge;
225 use jj_lib::op_store::RefTarget;
226
227 use super::*;
228
229 fn make_backend_commit(author: Signature, committer: Signature) -> Arc<backend::Commit> {
230 Arc::new(backend::Commit {
231 parents: vec![],
232 predecessors: vec![],
233 root_tree: Merge::resolved(TreeId::new(vec![])),
234 conflict_labels: Merge::resolved(String::new()),
235 change_id: ChangeId::new(vec![]),
236 description: String::new(),
237 author,
238 committer,
239 secure_sig: None,
240 })
241 }
242
243 fn make_default_signature() -> Signature {
244 Signature {
245 name: "Test User".to_owned(),
246 email: "test.user@g.com".to_owned(),
247 timestamp: Timestamp {
248 timestamp: MillisSinceEpoch(0),
249 tz_offset: 0,
250 },
251 }
252 }
253
254 fn commit_id_generator() -> impl FnMut() -> CommitId {
255 let mut iter = (1_u128..).map(|n| CommitId::new(n.to_le_bytes().into()));
256 move || iter.next().unwrap()
257 }
258
259 fn commit_ts_generator() -> impl FnMut() -> Timestamp {
260 let mut iter = Some(1_i64).into_iter().chain(1_i64..).map(|ms| Timestamp {
262 timestamp: MillisSinceEpoch(ms),
263 tz_offset: 0,
264 });
265 move || iter.next().unwrap()
266 }
267
268 fn prepare_data_sort_and_snapshot(sort_keys: &[SortKey]) -> String {
271 let mut new_commit_id = commit_id_generator();
272 let mut new_timestamp = commit_ts_generator();
273 let names = ["bob", "alice", "eve", "bob", "bob"];
274 let emails = [
275 "bob@g.com",
276 "alice@g.com",
277 "eve@g.com",
278 "bob@g.com",
279 "bob@g.com",
280 ];
281 let bookmark_names = ["feature", "bug-fix", "chore", "bug-fix", "feature"];
282 let remote_names = [None, Some("upstream"), None, Some("origin"), Some("origin")];
283 let deleted = [false, false, false, false, true];
284 let mut bookmark_items: Vec<RefListItem> = Vec::new();
285 let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();
286 for (&name, &email, bookmark_name, remote_name, &is_deleted) in
287 itertools::izip!(&names, &emails, &bookmark_names, &remote_names, &deleted)
288 {
289 let commit_id = new_commit_id();
290 let mut b_name = "foo";
291 let mut author = make_default_signature();
292 let mut committer = make_default_signature();
293
294 if sort_keys.contains(&SortKey::Name) || sort_keys.contains(&SortKey::NameDesc) {
295 b_name = bookmark_name;
296 }
297 if sort_keys.contains(&SortKey::AuthorName)
298 || sort_keys.contains(&SortKey::AuthorNameDesc)
299 {
300 author.name = String::from(name);
301 }
302 if sort_keys.contains(&SortKey::AuthorEmail)
303 || sort_keys.contains(&SortKey::AuthorEmailDesc)
304 {
305 author.email = String::from(email);
306 }
307 if sort_keys.contains(&SortKey::AuthorDate)
308 || sort_keys.contains(&SortKey::AuthorDateDesc)
309 {
310 author.timestamp = new_timestamp();
311 }
312 if sort_keys.contains(&SortKey::CommitterName)
313 || sort_keys.contains(&SortKey::CommitterNameDesc)
314 {
315 committer.name = String::from(name);
316 }
317 if sort_keys.contains(&SortKey::CommitterEmail)
318 || sort_keys.contains(&SortKey::CommitterEmailDesc)
319 {
320 committer.email = String::from(email);
321 }
322 if sort_keys.contains(&SortKey::CommitterDate)
323 || sort_keys.contains(&SortKey::CommitterDateDesc)
324 {
325 committer.timestamp = new_timestamp();
326 }
327
328 if let Some(remote_name) = remote_name {
329 if is_deleted {
330 bookmark_items.push(RefListItem {
331 primary: CommitRef::remote_only(b_name, *remote_name, RefTarget::absent()),
332 tracked: vec![CommitRef::local_only(
333 b_name,
334 RefTarget::normal(commit_id.clone()),
335 )],
336 });
337 } else {
338 bookmark_items.push(RefListItem {
339 primary: CommitRef::remote_only(
340 b_name,
341 *remote_name,
342 RefTarget::normal(commit_id.clone()),
343 ),
344 tracked: vec![],
345 });
346 }
347 } else {
348 bookmark_items.push(RefListItem {
349 primary: CommitRef::local_only(b_name, RefTarget::normal(commit_id.clone())),
350 tracked: vec![],
351 });
352 }
353
354 commits.insert(commit_id, make_backend_commit(author, committer));
355 }
356
357 bookmark_items.sort_by_key(|item| {
360 (
361 item.primary.name().to_owned(),
362 item.primary.remote_name().map(|name| name.to_owned()),
363 )
364 });
365
366 sort_and_snapshot(&mut bookmark_items, sort_keys, &commits)
367 }
368
369 fn sort_and_snapshot(
371 items: &mut [RefListItem],
372 sort_keys: &[SortKey],
373 commits: &HashMap<CommitId, Arc<backend::Commit>>,
374 ) -> String {
375 sort_inner(items, sort_keys, commits);
376
377 let to_commit = |item: &RefListItem| {
378 let id = item.primary.target().added_ids().next()?;
379 commits.get(id)
380 };
381
382 macro_rules! row_format {
383 ($($args:tt)*) => {
384 format!("{:<20}{:<16}{:<17}{:<14}{:<16}{:<17}{}", $($args)*)
385 }
386 }
387
388 let header = row_format!(
389 "Name",
390 "AuthorName",
391 "AuthorEmail",
392 "AuthorDate",
393 "CommitterName",
394 "CommitterEmail",
395 "CommitterDate"
396 );
397
398 let rows: Vec<String> = items
399 .iter()
400 .map(|item| {
401 let name = [Some(item.primary.name()), item.primary.remote_name()]
402 .iter()
403 .flatten()
404 .join("@");
405
406 let commit = to_commit(item);
407
408 let author_name = commit
409 .map(|c| c.author.name.clone())
410 .unwrap_or_else(|| String::from("-"));
411 let author_email = commit
412 .map(|c| c.author.email.clone())
413 .unwrap_or_else(|| String::from("-"));
414 let author_date = commit
415 .map(|c| c.author.timestamp.timestamp.0.to_string())
416 .unwrap_or_else(|| String::from("-"));
417
418 let committer_name = commit
419 .map(|c| c.committer.name.clone())
420 .unwrap_or_else(|| String::from("-"));
421 let committer_email = commit
422 .map(|c| c.committer.email.clone())
423 .unwrap_or_else(|| String::from("-"));
424 let committer_date = commit
425 .map(|c| c.committer.timestamp.timestamp.0.to_string())
426 .unwrap_or_else(|| String::from("-"));
427
428 row_format!(
429 name,
430 author_name,
431 author_email,
432 author_date,
433 committer_name,
434 committer_email,
435 committer_date
436 )
437 })
438 .collect();
439
440 let mut result = vec![header];
441 result.extend(rows);
442 result.join("\n")
443 }
444
445 #[test]
446 fn test_sort_by_name() {
447 insta::assert_snapshot!(
448 prepare_data_sort_and_snapshot(&[SortKey::Name]), @r"
449 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
450 bug-fix@origin Test User test.user@g.com 0 Test User test.user@g.com 0
451 bug-fix@upstream Test User test.user@g.com 0 Test User test.user@g.com 0
452 chore Test User test.user@g.com 0 Test User test.user@g.com 0
453 feature Test User test.user@g.com 0 Test User test.user@g.com 0
454 feature@origin - - - - - -
455 ");
456 }
457
458 #[test]
459 fn test_sort_by_name_desc() {
460 insta::assert_snapshot!(
461 prepare_data_sort_and_snapshot(&[SortKey::NameDesc]), @r"
462 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
463 feature@origin - - - - - -
464 feature Test User test.user@g.com 0 Test User test.user@g.com 0
465 chore Test User test.user@g.com 0 Test User test.user@g.com 0
466 bug-fix@upstream Test User test.user@g.com 0 Test User test.user@g.com 0
467 bug-fix@origin Test User test.user@g.com 0 Test User test.user@g.com 0
468 ");
469 }
470
471 #[test]
472 fn test_sort_by_author_name() {
473 insta::assert_snapshot!(
474 prepare_data_sort_and_snapshot(&[SortKey::AuthorName]), @r"
475 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
476 foo@origin - - - - - -
477 foo@upstream alice test.user@g.com 0 Test User test.user@g.com 0
478 foo bob test.user@g.com 0 Test User test.user@g.com 0
479 foo@origin bob test.user@g.com 0 Test User test.user@g.com 0
480 foo eve test.user@g.com 0 Test User test.user@g.com 0
481 ");
482 }
483
484 #[test]
485 fn test_sort_by_author_name_desc() {
486 insta::assert_snapshot!(
487 prepare_data_sort_and_snapshot(&[SortKey::AuthorNameDesc]), @r"
488 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
489 foo eve test.user@g.com 0 Test User test.user@g.com 0
490 foo bob test.user@g.com 0 Test User test.user@g.com 0
491 foo@origin bob test.user@g.com 0 Test User test.user@g.com 0
492 foo@upstream alice test.user@g.com 0 Test User test.user@g.com 0
493 foo@origin - - - - - -
494 ");
495 }
496
497 #[test]
498 fn test_sort_by_author_email() {
499 insta::assert_snapshot!(
500 prepare_data_sort_and_snapshot(&[SortKey::AuthorEmail]), @r"
501 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
502 foo@origin - - - - - -
503 foo@upstream Test User alice@g.com 0 Test User test.user@g.com 0
504 foo Test User bob@g.com 0 Test User test.user@g.com 0
505 foo@origin Test User bob@g.com 0 Test User test.user@g.com 0
506 foo Test User eve@g.com 0 Test User test.user@g.com 0
507 ");
508 }
509
510 #[test]
511 fn test_sort_by_author_email_desc() {
512 insta::assert_snapshot!(
513 prepare_data_sort_and_snapshot(&[SortKey::AuthorEmailDesc]), @r"
514 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
515 foo Test User eve@g.com 0 Test User test.user@g.com 0
516 foo Test User bob@g.com 0 Test User test.user@g.com 0
517 foo@origin Test User bob@g.com 0 Test User test.user@g.com 0
518 foo@upstream Test User alice@g.com 0 Test User test.user@g.com 0
519 foo@origin - - - - - -
520 ");
521 }
522
523 #[test]
524 fn test_sort_by_author_date() {
525 insta::assert_snapshot!(
526 prepare_data_sort_and_snapshot(&[SortKey::AuthorDate]), @r"
527 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
528 foo@origin - - - - - -
529 foo Test User test.user@g.com 1 Test User test.user@g.com 0
530 foo@upstream Test User test.user@g.com 1 Test User test.user@g.com 0
531 foo Test User test.user@g.com 2 Test User test.user@g.com 0
532 foo@origin Test User test.user@g.com 3 Test User test.user@g.com 0
533 ");
534 }
535
536 #[test]
537 fn test_sort_by_author_date_desc() {
538 insta::assert_snapshot!(
539 prepare_data_sort_and_snapshot(&[SortKey::AuthorDateDesc]), @r"
540 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
541 foo@origin Test User test.user@g.com 3 Test User test.user@g.com 0
542 foo Test User test.user@g.com 2 Test User test.user@g.com 0
543 foo Test User test.user@g.com 1 Test User test.user@g.com 0
544 foo@upstream Test User test.user@g.com 1 Test User test.user@g.com 0
545 foo@origin - - - - - -
546 ");
547 }
548
549 #[test]
550 fn test_sort_by_committer_name() {
551 insta::assert_snapshot!(
552 prepare_data_sort_and_snapshot(&[SortKey::CommitterName]), @r"
553 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
554 foo@origin - - - - - -
555 foo@upstream Test User test.user@g.com 0 alice test.user@g.com 0
556 foo Test User test.user@g.com 0 bob test.user@g.com 0
557 foo@origin Test User test.user@g.com 0 bob test.user@g.com 0
558 foo Test User test.user@g.com 0 eve test.user@g.com 0
559 ");
560 }
561
562 #[test]
563 fn test_sort_by_committer_name_desc() {
564 insta::assert_snapshot!(
565 prepare_data_sort_and_snapshot(&[SortKey::CommitterNameDesc]), @r"
566 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
567 foo Test User test.user@g.com 0 eve test.user@g.com 0
568 foo Test User test.user@g.com 0 bob test.user@g.com 0
569 foo@origin Test User test.user@g.com 0 bob test.user@g.com 0
570 foo@upstream Test User test.user@g.com 0 alice test.user@g.com 0
571 foo@origin - - - - - -
572 ");
573 }
574
575 #[test]
576 fn test_sort_by_committer_email() {
577 insta::assert_snapshot!(
578 prepare_data_sort_and_snapshot(&[SortKey::CommitterEmail]), @r"
579 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
580 foo@origin - - - - - -
581 foo@upstream Test User test.user@g.com 0 Test User alice@g.com 0
582 foo Test User test.user@g.com 0 Test User bob@g.com 0
583 foo@origin Test User test.user@g.com 0 Test User bob@g.com 0
584 foo Test User test.user@g.com 0 Test User eve@g.com 0
585 ");
586 }
587
588 #[test]
589 fn test_sort_by_committer_email_desc() {
590 insta::assert_snapshot!(
591 prepare_data_sort_and_snapshot(&[SortKey::CommitterEmailDesc]), @r"
592 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
593 foo Test User test.user@g.com 0 Test User eve@g.com 0
594 foo Test User test.user@g.com 0 Test User bob@g.com 0
595 foo@origin Test User test.user@g.com 0 Test User bob@g.com 0
596 foo@upstream Test User test.user@g.com 0 Test User alice@g.com 0
597 foo@origin - - - - - -
598 ");
599 }
600
601 #[test]
602 fn test_sort_by_committer_date() {
603 insta::assert_snapshot!(
604 prepare_data_sort_and_snapshot(&[SortKey::CommitterDate]), @r"
605 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
606 foo@origin - - - - - -
607 foo Test User test.user@g.com 0 Test User test.user@g.com 1
608 foo@upstream Test User test.user@g.com 0 Test User test.user@g.com 1
609 foo Test User test.user@g.com 0 Test User test.user@g.com 2
610 foo@origin Test User test.user@g.com 0 Test User test.user@g.com 3
611 ");
612 }
613
614 #[test]
615 fn test_sort_by_committer_date_desc() {
616 insta::assert_snapshot!(
617 prepare_data_sort_and_snapshot(&[SortKey::CommitterDateDesc]), @r"
618 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
619 foo@origin Test User test.user@g.com 0 Test User test.user@g.com 3
620 foo Test User test.user@g.com 0 Test User test.user@g.com 2
621 foo Test User test.user@g.com 0 Test User test.user@g.com 1
622 foo@upstream Test User test.user@g.com 0 Test User test.user@g.com 1
623 foo@origin - - - - - -
624 ");
625 }
626
627 #[test]
628 fn test_sort_by_author_date_desc_and_name() {
629 insta::assert_snapshot!(
630 prepare_data_sort_and_snapshot(&[SortKey::AuthorDateDesc, SortKey::Name]), @r"
631 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
632 bug-fix@origin Test User test.user@g.com 3 Test User test.user@g.com 0
633 chore Test User test.user@g.com 2 Test User test.user@g.com 0
634 bug-fix@upstream Test User test.user@g.com 1 Test User test.user@g.com 0
635 feature Test User test.user@g.com 1 Test User test.user@g.com 0
636 feature@origin - - - - - -
637 ");
638 }
639
640 #[test]
641 fn test_sort_by_committer_name_and_name_desc() {
642 insta::assert_snapshot!(
643 prepare_data_sort_and_snapshot(&[SortKey::CommitterName, SortKey::NameDesc]), @r"
644 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
645 feature@origin - - - - - -
646 bug-fix@upstream Test User test.user@g.com 0 alice test.user@g.com 0
647 feature Test User test.user@g.com 0 bob test.user@g.com 0
648 bug-fix@origin Test User test.user@g.com 0 bob test.user@g.com 0
649 chore Test User test.user@g.com 0 eve test.user@g.com 0
650 ");
651 }
652
653 #[test]
656 fn test_sort_by_name_and_committer_date() {
657 insta::assert_snapshot!(
658 prepare_data_sort_and_snapshot(&[SortKey::Name, SortKey::AuthorDate]), @r"
659 Name AuthorName AuthorEmail AuthorDate CommitterName CommitterEmail CommitterDate
660 bug-fix@origin Test User test.user@g.com 3 Test User test.user@g.com 0
661 bug-fix@upstream Test User test.user@g.com 1 Test User test.user@g.com 0
662 chore Test User test.user@g.com 2 Test User test.user@g.com 0
663 feature Test User test.user@g.com 1 Test User test.user@g.com 0
664 feature@origin - - - - - -
665 ");
666 }
667}