Changeset 13865
- Timestamp:
- Jan 25, 2013, 6:01:45 AM (8 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/libtransmission/makemeta.c
r13800 r13865 40 40 struct FileList 41 41 { 42 struct FileList * next;43 uint64_t size;44 char * filename;42 uint64_t size; 43 char * filename; 44 struct FileList * next; 45 45 }; 46 46 47 static struct FileList *48 getFiles (const char *dir,49 const char *base,47 static struct FileList * 48 getFiles (const char * dir, 49 const char * base, 50 50 struct FileList * list) 51 51 { 52 inti;53 char * buf;54 struct stat sb;55 DIR * odir = NULL;56 57 58 59 60 61 62 { 63 52 int i; 53 DIR * odir; 54 char * buf; 55 struct stat sb; 56 57 sb.st_size = 0; 58 59 buf = tr_buildPath (dir, base, NULL); 60 i = stat (buf, &sb); 61 if (i) 62 { 63 tr_err (_("Torrent Creator is skipping file \"%s\": %s"), 64 64 buf, tr_strerror (errno)); 65 66 67 } 68 69 70 { 71 72 73 74 75 76 } 77 78 { 79 80 81 82 83 84 } 85 86 87 65 tr_free (buf); 66 return list; 67 } 68 69 if (S_ISDIR (sb.st_mode) && ((odir = opendir (buf)))) 70 { 71 struct dirent *d; 72 for (d = readdir (odir); d != NULL; d = readdir (odir)) 73 if (d->d_name && d->d_name[0] != '.') /* skip dotfiles */ 74 list = getFiles (buf, d->d_name, list); 75 closedir (odir); 76 } 77 else if (S_ISREG (sb.st_mode) && (sb.st_size > 0)) 78 { 79 struct FileList * node = tr_new (struct FileList, 1); 80 node->size = sb.st_size; 81 node->filename = tr_strdup (buf); 82 node->next = list; 83 list = node; 84 } 85 86 tr_free (buf); 87 return list; 88 88 } 89 89 … … 91 91 bestPieceSize (uint64_t totalSize) 92 92 { 93 94 95 96 97 98 99 100 101 102 103 93 const uint32_t KiB = 1024; 94 const uint32_t MiB = 1048576; 95 const uint32_t GiB = 1073741824; 96 97 if (totalSize >= (2 * GiB)) return 2 * MiB; 98 if (totalSize >= (1 * GiB)) return 1 * MiB; 99 if (totalSize >= (512 * MiB)) return 512 * KiB; 100 if (totalSize >= (350 * MiB)) return 256 * KiB; 101 if (totalSize >= (150 * MiB)) return 128 * KiB; 102 if (totalSize >= (50 * MiB)) return 64 * KiB; 103 return 32 * KiB; /* less than 50 meg */ 104 104 } 105 105 … … 107 107 builderFileCompare (const void * va, const void * vb) 108 108 { 109 110 111 112 109 const tr_metainfo_builder_file * a = va; 110 const tr_metainfo_builder_file * b = vb; 111 112 return evutil_ascii_strcasecmp (a->filename, b->filename); 113 113 } 114 114 … … 116 116 tr_metaInfoBuilderCreate (const char * topFileArg) 117 117 { 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 for (walk = files; walk != NULL; walk =walk->next)145 146 147 148 149 for (i = 0, walk = files; walk !=NULL; ++i)150 { 151 struct FileList *tmp = walk;152 153 154 155 156 157 158 } 159 160 161 162 163 164 165 166 167 118 int i; 119 struct FileList * files; 120 struct FileList * walk; 121 char topFile[TR_PATH_MAX]; 122 tr_metainfo_builder * ret = tr_new0 (tr_metainfo_builder, 1); 123 124 tr_realpath (topFileArg, topFile); 125 126 ret->top = tr_strdup (topFile); 127 128 { 129 struct stat sb; 130 stat (topFile, &sb); 131 ret->isSingleFile = !S_ISDIR (sb.st_mode); 132 } 133 134 /* build a list of files containing topFile and, 135 if it's a directory, all of its children */ 136 { 137 char * dir = tr_dirname (topFile); 138 char * base = tr_basename (topFile); 139 files = getFiles (dir, base, NULL); 140 tr_free (base); 141 tr_free (dir); 142 } 143 144 for (walk=files; walk!=NULL; walk=walk->next) 145 ++ret->fileCount; 146 147 ret->files = tr_new0 (tr_metainfo_builder_file, ret->fileCount); 148 149 for (i=0, walk=files; walk!=NULL; ++i) 150 { 151 struct FileList * tmp = walk; 152 tr_metainfo_builder_file * file = &ret->files[i]; 153 walk = walk->next; 154 file->filename = tmp->filename; 155 file->size = tmp->size; 156 ret->totalSize += tmp->size; 157 tr_free (tmp); 158 } 159 160 qsort (ret->files, 161 ret->fileCount, 162 sizeof (tr_metainfo_builder_file), 163 builderFileCompare); 164 165 tr_metaInfoBuilderSetPieceSize (ret, bestPieceSize (ret->totalSize)); 166 167 return ret; 168 168 } 169 169 … … 183 183 tr_metaInfoBuilderFree (tr_metainfo_builder * builder) 184 184 { 185 if (builder) 186 { 187 tr_file_index_t t; 188 int i; 189 for (t = 0; t < builder->fileCount; ++t) 190 tr_free (builder->files[t].filename); 191 tr_free (builder->files); 192 tr_free (builder->top); 193 tr_free (builder->comment); 194 for (i = 0; i < builder->trackerCount; ++i) 195 tr_free (builder->trackers[i].announce); 196 tr_free (builder->trackers); 197 tr_free (builder->outputFile); 198 tr_free (builder); 185 if (builder) 186 { 187 int i; 188 tr_file_index_t t; 189 190 for (t=0; t<builder->fileCount; ++t) 191 tr_free (builder->files[t].filename); 192 tr_free (builder->files); 193 tr_free (builder->top); 194 tr_free (builder->comment); 195 for (i=0; i<builder->trackerCount; ++i) 196 tr_free (builder->trackers[i].announce); 197 tr_free (builder->trackers); 198 tr_free (builder->outputFile); 199 tr_free (builder); 199 200 } 200 201 } … … 207 208 getHashInfo (tr_metainfo_builder * b) 208 209 { 209 uint32_t fileIndex = 0; 210 uint8_t *ret = tr_new0 (uint8_t, SHA_DIGEST_LENGTH * b->pieceCount); 211 uint8_t *walk = ret; 212 uint8_t *buf; 213 uint64_t totalRemain; 214 uint64_t off = 0; 215 int fd; 216 217 if (!b->totalSize) 218 return ret; 219 220 buf = tr_valloc (b->pieceSize); 221 b->pieceIndex = 0; 222 totalRemain = b->totalSize; 223 fd = tr_open_file_for_scanning (b->files[fileIndex].filename); 224 if (fd < 0) 225 { 226 b->my_errno = errno; 227 tr_strlcpy (b->errfile, 228 b->files[fileIndex].filename, 229 sizeof (b->errfile)); 230 b->result = TR_MAKEMETA_IO_READ; 231 tr_free (buf); 232 tr_free (ret); 233 return NULL; 234 } 235 while (totalRemain) 236 { 237 uint8_t * bufptr = buf; 238 const uint32_t thisPieceSize = (uint32_t) MIN (b->pieceSize, totalRemain); 239 uint32_t leftInPiece = thisPieceSize; 240 241 assert (b->pieceIndex < b->pieceCount); 242 243 while (leftInPiece) 244 { 245 const size_t n_this_pass = (size_t) MIN ((b->files[fileIndex].size - off), leftInPiece); 246 const ssize_t n_read = read (fd, bufptr, n_this_pass); 247 bufptr += n_read; 248 off += n_read; 249 leftInPiece -= n_read; 250 if (off == b->files[fileIndex].size) 210 uint32_t fileIndex = 0; 211 uint8_t *ret = tr_new0 (uint8_t, SHA_DIGEST_LENGTH * b->pieceCount); 212 uint8_t *walk = ret; 213 uint8_t *buf; 214 uint64_t totalRemain; 215 uint64_t off = 0; 216 int fd; 217 218 if (!b->totalSize) 219 return ret; 220 221 buf = tr_valloc (b->pieceSize); 222 b->pieceIndex = 0; 223 totalRemain = b->totalSize; 224 fd = tr_open_file_for_scanning (b->files[fileIndex].filename); 225 if (fd < 0) 226 { 227 b->my_errno = errno; 228 tr_strlcpy (b->errfile, 229 b->files[fileIndex].filename, 230 sizeof (b->errfile)); 231 b->result = TR_MAKEMETA_IO_READ; 232 tr_free (buf); 233 tr_free (ret); 234 return NULL; 235 } 236 237 while (totalRemain) 238 { 239 uint8_t * bufptr = buf; 240 const uint32_t thisPieceSize = (uint32_t) MIN (b->pieceSize, totalRemain); 241 uint32_t leftInPiece = thisPieceSize; 242 243 assert (b->pieceIndex < b->pieceCount); 244 245 while (leftInPiece) 246 { 247 const size_t n_this_pass = (size_t) MIN ((b->files[fileIndex].size - off), leftInPiece); 248 const ssize_t n_read = read (fd, bufptr, n_this_pass); 249 bufptr += n_read; 250 off += n_read; 251 leftInPiece -= n_read; 252 if (off == b->files[fileIndex].size) 251 253 { 252 253 254 255 254 off = 0; 255 tr_close_file (fd); 256 fd = -1; 257 if (++fileIndex < b->fileCount) 256 258 { 257 258 259 fd = tr_open_file_for_scanning (b->files[fileIndex].filename); 260 if (fd < 0) 259 261 { 260 261 262 263 264 265 266 267 262 b->my_errno = errno; 263 tr_strlcpy (b->errfile, 264 b->files[fileIndex].filename, 265 sizeof (b->errfile)); 266 b->result = TR_MAKEMETA_IO_READ; 267 tr_free (buf); 268 tr_free (ret); 269 return NULL; 268 270 } 269 271 } … … 271 273 } 272 274 273 274 275 276 277 278 279 { 280 281 282 } 283 284 285 286 } 287 288 289 290 291 292 293 294 295 296 275 assert (bufptr - buf == (int)thisPieceSize); 276 assert (leftInPiece == 0); 277 tr_sha1 (walk, buf, thisPieceSize, NULL); 278 walk += SHA_DIGEST_LENGTH; 279 280 if (b->abortFlag) 281 { 282 b->result = TR_MAKEMETA_CANCELLED; 283 break; 284 } 285 286 totalRemain -= thisPieceSize; 287 ++b->pieceIndex; 288 } 289 290 assert (b->abortFlag 291 || (walk - ret == (int)(SHA_DIGEST_LENGTH * b->pieceCount))); 292 assert (b->abortFlag || !totalRemain); 293 294 if (fd >= 0) 295 tr_close_file (fd); 296 297 tr_free (buf); 298 return ret; 297 299 } 298 300 299 301 static void 300 getFileInfo (const char * topFile, 301 const tr_metainfo_builder_file * file, 302 tr_variant * uninitialized_length, 303 tr_variant * uninitialized_path) 304 { 305 size_t offset; 306 307 /* get the file size */ 308 tr_variantInitInt (uninitialized_length, file->size); 309 310 /* how much of file->filename to walk past */ 311 offset = strlen (topFile); 312 if (offset>0 && topFile[offset-1]!=TR_PATH_DELIMITER) 313 ++offset; /* +1 for the path delimiter */ 314 315 /* build the path list */ 316 tr_variantInitList (uninitialized_path, 0); 317 if (strlen (file->filename) > offset) { 318 char * filename = tr_strdup (file->filename + offset); 319 char * walk = filename; 320 const char * token; 321 while ((token = tr_strsep (&walk, TR_PATH_DELIMITER_STR))) 322 tr_variantListAddStr (uninitialized_path, token); 323 tr_free (filename); 302 getFileInfo (const char * topFile, 303 const tr_metainfo_builder_file * file, 304 tr_variant * uninitialized_length, 305 tr_variant * uninitialized_path) 306 { 307 size_t offset; 308 309 /* get the file size */ 310 tr_variantInitInt (uninitialized_length, file->size); 311 312 /* how much of file->filename to walk past */ 313 offset = strlen (topFile); 314 if (offset>0 && topFile[offset-1]!=TR_PATH_DELIMITER) 315 ++offset; /* +1 for the path delimiter */ 316 317 /* build the path list */ 318 tr_variantInitList (uninitialized_path, 0); 319 if (strlen (file->filename) > offset) 320 { 321 char * filename = tr_strdup (file->filename + offset); 322 char * walk = filename; 323 const char * token; 324 while ((token = tr_strsep (&walk, TR_PATH_DELIMITER_STR))) 325 tr_variantListAddStr (uninitialized_path, token); 326 tr_free (filename); 324 327 } 325 328 } 326 329 327 330 static void 328 makeInfoDict (tr_variant *dict,331 makeInfoDict (tr_variant * dict, 329 332 tr_metainfo_builder * builder) 330 333 { 331 uint8_t * pch; 332 char * base; 333 334 tr_variantDictReserve (dict, 5); 335 336 if (builder->isSingleFile) 337 { 338 tr_variantDictAddInt (dict, TR_KEY_length, builder->files[0].size); 339 } 340 else /* root node is a directory */ 341 { 342 uint32_t i; 343 tr_variant * list = tr_variantDictAddList (dict, TR_KEY_files, 344 builder->fileCount); 345 for (i = 0; i < builder->fileCount; ++i) 346 { 347 tr_variant * d = tr_variantListAddDict (list, 2); 348 tr_variant * length = tr_variantDictAdd (d, TR_KEY_length); 349 tr_variant * pathVal = tr_variantDictAdd (d, TR_KEY_path); 350 getFileInfo (builder->top, &builder->files[i], length, pathVal); 351 } 352 } 353 354 base = tr_basename (builder->top); 355 tr_variantDictAddStr (dict, TR_KEY_name, base); 356 tr_free (base); 357 358 tr_variantDictAddInt (dict, TR_KEY_piece_length, builder->pieceSize); 359 360 if ((pch = getHashInfo (builder))) 361 { 362 tr_variantDictAddRaw (dict, TR_KEY_pieces, pch, 363 SHA_DIGEST_LENGTH * builder->pieceCount); 364 tr_free (pch); 365 } 366 367 tr_variantDictAddInt (dict, TR_KEY_private, builder->isPrivate ? 1 : 0); 334 char * base; 335 uint8_t * pch; 336 337 tr_variantDictReserve (dict, 5); 338 339 if (builder->isSingleFile) 340 { 341 tr_variantDictAddInt (dict, TR_KEY_length, builder->files[0].size); 342 } 343 else /* root node is a directory */ 344 { 345 uint32_t i; 346 tr_variant * list = tr_variantDictAddList (dict, TR_KEY_files, 347 builder->fileCount); 348 for (i=0; i<builder->fileCount; ++i) 349 { 350 tr_variant * d = tr_variantListAddDict (list, 2); 351 tr_variant * length = tr_variantDictAdd (d, TR_KEY_length); 352 tr_variant * pathVal = tr_variantDictAdd (d, TR_KEY_path); 353 getFileInfo (builder->top, &builder->files[i], length, pathVal); 354 } 355 } 356 357 base = tr_basename (builder->top); 358 tr_variantDictAddStr (dict, TR_KEY_name, base); 359 tr_free (base); 360 361 tr_variantDictAddInt (dict, TR_KEY_piece_length, builder->pieceSize); 362 363 if ((pch = getHashInfo (builder))) 364 { 365 tr_variantDictAddRaw (dict, TR_KEY_pieces, 366 pch, 367 SHA_DIGEST_LENGTH * builder->pieceCount); 368 tr_free (pch); 369 } 370 371 tr_variantDictAddInt (dict, TR_KEY_private, builder->isPrivate ? 1 : 0); 368 372 } 369 373 … … 371 375 tr_realMakeMetaInfo (tr_metainfo_builder * builder) 372 376 { 373 int i; 374 tr_variant top; 375 376 /* allow an empty set, but if URLs *are* listed, verify them. #814, #971 */ 377 for (i = 0; i < builder->trackerCount && !builder->result; ++i) { 378 if (!tr_urlIsValidTracker (builder->trackers[i].announce)) { 379 tr_strlcpy (builder->errfile, builder->trackers[i].announce, 380 sizeof (builder->errfile)); 381 builder->result = TR_MAKEMETA_URL; 382 } 383 } 384 385 tr_variantInitDict (&top, 6); 386 387 if (!builder->fileCount || !builder->totalSize || 388 !builder->pieceSize || !builder->pieceCount) 389 { 390 builder->errfile[0] = '\0'; 391 builder->my_errno = ENOENT; 392 builder->result = TR_MAKEMETA_IO_READ; 393 builder->isDone = true; 394 } 395 396 if (!builder->result && builder->trackerCount) 397 { 398 int prevTier = -1; 399 tr_variant * tier = NULL; 400 401 if (builder->trackerCount > 1) 402 { 403 tr_variant * annList = tr_variantDictAddList (&top, TR_KEY_announce_list, 0); 404 for (i = 0; i < builder->trackerCount; ++i) 377 int i; 378 tr_variant top; 379 380 /* allow an empty set, but if URLs *are* listed, verify them. #814, #971 */ 381 for (i=0; i<builder->trackerCount && !builder->result; ++i) 382 { 383 if (!tr_urlIsValidTracker (builder->trackers[i].announce)) 384 { 385 tr_strlcpy (builder->errfile, builder->trackers[i].announce, 386 sizeof (builder->errfile)); 387 builder->result = TR_MAKEMETA_URL; 388 } 389 } 390 391 tr_variantInitDict (&top, 6); 392 393 if (!builder->fileCount || !builder->totalSize || 394 !builder->pieceSize || !builder->pieceCount) 395 { 396 builder->errfile[0] = '\0'; 397 builder->my_errno = ENOENT; 398 builder->result = TR_MAKEMETA_IO_READ; 399 builder->isDone = true; 400 } 401 402 if (!builder->result && builder->trackerCount) 403 { 404 int prevTier = -1; 405 tr_variant * tier = NULL; 406 407 if (builder->trackerCount > 1) 408 { 409 tr_variant * annList = tr_variantDictAddList (&top, TR_KEY_announce_list, 0); 410 for (i=0; i<builder->trackerCount; ++i) 405 411 { 406 412 if (prevTier != builder->trackers[i].tier) 407 413 { 408 409 414 prevTier = builder->trackers[i].tier; 415 tier = tr_variantListAddList (annList, 0); 410 416 } 411 417 tr_variantListAddStr (tier, builder->trackers[i].announce); 412 418 } 413 419 } 414 420 415 416 } 417 418 419 { 420 421 422 423 TR_NAME "/" LONG_VERSION_STRING);424 425 426 427 } 428 429 430 431 { 432 433 { 434 435 436 437 438 } 439 } 440 441 442 443 444 445 421 tr_variantDictAddStr (&top, TR_KEY_announce, builder->trackers[0].announce); 422 } 423 424 if (!builder->result && !builder->abortFlag) 425 { 426 if (builder->comment && *builder->comment) 427 tr_variantDictAddStr (&top, TR_KEY_comment, builder->comment); 428 tr_variantDictAddStr (&top, TR_KEY_created_by, 429 TR_NAME "/" LONG_VERSION_STRING); 430 tr_variantDictAddInt (&top, TR_KEY_creation_date, time (NULL)); 431 tr_variantDictAddStr (&top, TR_KEY_encoding, "UTF-8"); 432 makeInfoDict (tr_variantDictAddDict (&top, TR_KEY_info, 666), builder); 433 } 434 435 /* save the file */ 436 if (!builder->result && !builder->abortFlag) 437 { 438 if (tr_variantToFile (&top, TR_VARIANT_FMT_BENC, builder->outputFile)) 439 { 440 builder->my_errno = errno; 441 tr_strlcpy (builder->errfile, builder->outputFile, 442 sizeof (builder->errfile)); 443 builder->result = TR_MAKEMETA_IO_WRITE; 444 } 445 } 446 447 /* cleanup */ 448 tr_variantFree (&top); 449 if (builder->abortFlag) 450 builder->result = TR_MAKEMETA_CANCELLED; 451 builder->isDone = 1; 446 452 } 447 453 … … 454 460 static tr_metainfo_builder * queue = NULL; 455 461 456 static tr_thread * 462 static tr_thread * workerThread = NULL; 457 463 458 464 static tr_lock* 459 465 getQueueLock (void) 460 466 { 461 462 463 464 465 466 467 static tr_lock * lock = NULL; 468 469 if (!lock) 470 lock = tr_lockNew (); 471 472 return lock; 467 473 } 468 474 … … 470 476 makeMetaWorkerFunc (void * unused UNUSED) 471 477 { 472 473 { 474 475 476 477 478 479 480 { 481 482 483 } 484 485 486 487 488 489 490 491 } 492 493 478 for (;;) 479 { 480 tr_metainfo_builder * builder = NULL; 481 482 /* find the next builder to process */ 483 tr_lock * lock = getQueueLock (); 484 tr_lockLock (lock); 485 if (queue) 486 { 487 builder = queue; 488 queue = queue->nextBuilder; 489 } 490 tr_lockUnlock (lock); 491 492 /* if no builders, this worker thread is done */ 493 if (builder == NULL) 494 break; 495 496 tr_realMakeMetaInfo (builder); 497 } 498 499 workerThread = NULL; 494 500 } 495 501 496 502 void 497 tr_makeMetaInfo (tr_metainfo_builder *builder,498 const char *outputFile,503 tr_makeMetaInfo (tr_metainfo_builder * builder, 504 const char * outputFile, 499 505 const tr_tracker_info * trackers, 500 506 int trackerCount, 501 const char *comment,507 const char * comment, 502 508 int isPrivate) 503 509 { 504 int i; 505 tr_lock * lock; 506 507 /* free any variables from a previous run */ 508 for (i = 0; i < builder->trackerCount; ++i) 509 tr_free (builder->trackers[i].announce); 510 tr_free (builder->trackers); 511 tr_free (builder->comment); 512 tr_free (builder->outputFile); 513 514 /* initialize the builder variables */ 515 builder->abortFlag = 0; 516 builder->result = 0; 517 builder->isDone = 0; 518 builder->pieceIndex = 0; 519 builder->trackerCount = trackerCount; 520 builder->trackers = tr_new0 (tr_tracker_info, builder->trackerCount); 521 for (i = 0; i < builder->trackerCount; ++i) { 522 builder->trackers[i].tier = trackers[i].tier; 523 builder->trackers[i].announce = tr_strdup (trackers[i].announce); 524 } 525 builder->comment = tr_strdup (comment); 526 builder->isPrivate = isPrivate; 527 if (outputFile && *outputFile) 528 builder->outputFile = tr_strdup (outputFile); 529 else 530 builder->outputFile = tr_strdup_printf ("%s.torrent", builder->top); 531 532 /* enqueue the builder */ 533 lock = getQueueLock (); 534 tr_lockLock (lock); 535 builder->nextBuilder = queue; 536 queue = builder; 537 if (!workerThread) 538 workerThread = tr_threadNew (makeMetaWorkerFunc, NULL); 539 tr_lockUnlock (lock); 540 } 541 510 int i; 511 tr_lock * lock; 512 513 /* free any variables from a previous run */ 514 for (i=0; i<builder->trackerCount; ++i) 515 tr_free (builder->trackers[i].announce); 516 tr_free (builder->trackers); 517 tr_free (builder->comment); 518 tr_free (builder->outputFile); 519 520 /* initialize the builder variables */ 521 builder->abortFlag = 0; 522 builder->result = 0; 523 builder->isDone = 0; 524 builder->pieceIndex = 0; 525 builder->trackerCount = trackerCount; 526 builder->trackers = tr_new0 (tr_tracker_info, builder->trackerCount); 527 for (i=0; i<builder->trackerCount; ++i) 528 { 529 builder->trackers[i].tier = trackers[i].tier; 530 builder->trackers[i].announce = tr_strdup (trackers[i].announce); 531 } 532 builder->comment = tr_strdup (comment); 533 builder->isPrivate = isPrivate; 534 if (outputFile && *outputFile) 535 builder->outputFile = tr_strdup (outputFile); 536 else 537 builder->outputFile = tr_strdup_printf ("%s.torrent", builder->top); 538 539 /* enqueue the builder */ 540 lock = getQueueLock (); 541 tr_lockLock (lock); 542 builder->nextBuilder = queue; 543 queue = builder; 544 if (!workerThread) 545 workerThread = tr_threadNew (makeMetaWorkerFunc, NULL); 546 tr_lockUnlock (lock); 547 } 548
Note: See TracChangeset
for help on using the changeset viewer.