resulgit 1.0.19 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +143 -0
  2. package/package.json +1 -1
  3. package/resulgit.js +237 -59
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # resulgit
2
+
3
+ A powerful command-line interface (CLI) tool for version control system operations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g resulgit
9
+ ```
10
+
11
+ Or install locally:
12
+
13
+ ```bash
14
+ npm install resulgit
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ resulgit <command> [options]
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ ### Authentication
26
+
27
+ - `resulgit auth set-token --token <token>` - Set authentication token
28
+ - `resulgit auth set-server --server <url>` - Set server URL
29
+ - `resulgit auth login --email <email> --password <password>` - Login to server
30
+ - `resulgit auth register --username <name> --email <email> --password <password>` - Register new account
31
+
32
+ ### Repository Management
33
+
34
+ - `resulgit repo list` - List all repositories
35
+ - `resulgit repo create --name <name> [--description <text>] [--visibility <private|public>]` - Create new repository
36
+ - `resulgit repo log --repo <id> [--branch <name>]` - View commit log
37
+ - `resulgit repo head --repo <id> [--branch <name>]` - Get HEAD commit
38
+ - `resulgit repo select` - Interactive repository selection
39
+
40
+ ### Clone & Workspace
41
+
42
+ - `resulgit clone --repo <id> --branch <name> [--dest <dir>]` - Clone a repository
43
+ - `resulgit workspace set-root --path <dir>` - Set workspace root directory
44
+
45
+ ### Branch Operations
46
+
47
+ - `resulgit branch list` - List all branches
48
+ - `resulgit branch create --name <branch> [--base <branch>]` - Create new branch
49
+ - `resulgit branch delete --name <branch>` - Delete a branch
50
+ - `resulgit switch --branch <name>` - Switch to a branch
51
+ - `resulgit checkout --branch <name>` - Checkout a branch
52
+
53
+ ### File Operations
54
+
55
+ - `resulgit status` - Show working directory status
56
+ - `resulgit diff [--path <file>] [--commit <id>]` - Show differences
57
+ - `resulgit add <file> [--content <text>] [--all]` - Add files
58
+ - `resulgit rm --path <file>` - Remove files
59
+ - `resulgit mv --from <old> --to <new>` - Move/rename files
60
+ - `resulgit restore --path <file> [--source <commit>]` - Restore file from commit
61
+
62
+ ### Version Control
63
+
64
+ - `resulgit commit --message <text>` - Create a commit
65
+ - `resulgit push` - Push changes to remote
66
+ - `resulgit pull` - Pull changes from remote
67
+ - `resulgit merge --branch <name> [--squash] [--no-push]` - Merge branches
68
+ - `resulgit cherry-pick --commit <id> [--branch <name>] [--no-push]` - Cherry-pick a commit
69
+ - `resulgit revert --commit <id> [--no-push]` - Revert a commit
70
+ - `resulgit reset [--commit <id>] [--mode <soft|mixed|hard>]` - Reset to commit
71
+
72
+ ### Stash Operations
73
+
74
+ - `resulgit stash` or `resulgit stash save [--message <msg>]` - Save changes to stash
75
+ - `resulgit stash list` - List all stashes
76
+ - `resulgit stash pop [--index <n>]` - Apply and remove stash
77
+ - `resulgit stash apply [--index <n>]` - Apply stash without removing
78
+ - `resulgit stash drop [--index <n>]` - Delete a stash
79
+ - `resulgit stash clear` - Clear all stashes
80
+
81
+ ### Tags
82
+
83
+ - `resulgit tag list` - List all tags
84
+ - `resulgit tag create --name <tag> [--branch <name>]` - Create a tag
85
+ - `resulgit tag delete --name <tag>` - Delete a tag
86
+
87
+ ### Pull Requests
88
+
89
+ - `resulgit pr list` - List pull requests
90
+ - `resulgit pr create --title <title> [--source <branch>] [--target <branch>]` - Create pull request
91
+ - `resulgit pr merge --id <id>` - Merge a pull request
92
+
93
+ ### Information
94
+
95
+ - `resulgit current` - Show current repository and branch
96
+ - `resulgit head` - Show HEAD commit ID
97
+ - `resulgit show --commit <id>` - Show commit details
98
+
99
+ ## Global Options
100
+
101
+ - `--server <url>` - Override default server
102
+ - `--token <token>` - Override stored token
103
+ - `--json` - Output in JSON format
104
+ - `--dir <path>` - Specify working directory
105
+
106
+ ## Examples
107
+
108
+ ```bash
109
+ # Login to server
110
+ resulgit auth login --email user@example.com --password mypassword
111
+
112
+ # List repositories
113
+ resulgit repo list
114
+
115
+ # Clone a repository
116
+ resulgit clone --repo 123 --branch main
117
+
118
+ # Check status
119
+ resulgit status
120
+
121
+ # Create and commit changes
122
+ resulgit add file.txt --content "Hello World"
123
+ resulgit commit --message "Add file.txt"
124
+ resulgit push
125
+
126
+ # Create a branch
127
+ resulgit branch create --name feature-branch
128
+
129
+ # Merge branches
130
+ resulgit merge --branch feature-branch
131
+ ```
132
+
133
+ ## Configuration
134
+
135
+ Configuration is stored in `~/.resulgit/config.json`. You can set:
136
+ - `server`: Default server URL
137
+ - `token`: Authentication token
138
+ - `workspaceRoot`: Default workspace directory
139
+
140
+ ## License
141
+
142
+ MIT
143
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resulgit",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "A powerful CLI tool for version control system operations - clone, commit, push, pull, merge, branch management, and more",
5
5
  "main": "resulgit.js",
6
6
  "bin": {
package/resulgit.js CHANGED
@@ -339,15 +339,134 @@ function hashContent(buf) {
339
339
  return crypto.createHash('sha1').update(buf).digest('hex')
340
340
  }
341
341
 
342
+ // LCS-based diff: computes edit script (list of equal/insert/delete operations)
343
+ function computeDiff(oldLines, newLines) {
344
+ const m = oldLines.length
345
+ const n = newLines.length
346
+ // Build LCS table
347
+ const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1))
348
+ for (let i = 1; i <= m; i++) {
349
+ for (let j = 1; j <= n; j++) {
350
+ if (oldLines[i - 1] === newLines[j - 1]) {
351
+ dp[i][j] = dp[i - 1][j - 1] + 1
352
+ } else {
353
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
354
+ }
355
+ }
356
+ }
357
+ // Backtrack to produce edit operations
358
+ const ops = [] // { type: 'equal'|'delete'|'insert', oldIdx, newIdx, line }
359
+ let i = m, j = n
360
+ while (i > 0 || j > 0) {
361
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
362
+ ops.push({ type: 'equal', oldIdx: i - 1, newIdx: j - 1, line: oldLines[i - 1] })
363
+ i--; j--
364
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
365
+ ops.push({ type: 'insert', newIdx: j - 1, line: newLines[j - 1] })
366
+ j--
367
+ } else {
368
+ ops.push({ type: 'delete', oldIdx: i - 1, line: oldLines[i - 1] })
369
+ i--
370
+ }
371
+ }
372
+ ops.reverse()
373
+ return ops
374
+ }
375
+
376
+ // Print unified diff with context lines and @@ hunk headers
377
+ function printUnifiedDiff(oldLines, newLines, contextLines) {
378
+ const ctx = contextLines !== undefined ? contextLines : 3
379
+ const ops = computeDiff(oldLines, newLines)
380
+ // Group ops into hunks (runs of changes with context)
381
+ const hunks = []
382
+ let hunk = null
383
+ let lastChangeEnd = -1
384
+ for (let k = 0; k < ops.length; k++) {
385
+ const op = ops[k]
386
+ if (op.type !== 'equal') {
387
+ // Start or extend a hunk
388
+ const contextStart = Math.max(0, k - ctx)
389
+ if (hunk && contextStart <= lastChangeEnd + ctx) {
390
+ // Extend current hunk
391
+ } else {
392
+ // Save previous hunk and start new
393
+ if (hunk) hunks.push(hunk)
394
+ hunk = { startIdx: Math.max(0, k - ctx), endIdx: k }
395
+ }
396
+ hunk.endIdx = k
397
+ lastChangeEnd = k
398
+ }
399
+ }
400
+ if (hunk) {
401
+ hunk.endIdx = Math.min(ops.length - 1, hunk.endIdx + ctx)
402
+ hunks.push(hunk)
403
+ }
404
+ // Print each hunk
405
+ for (const h of hunks) {
406
+ const start = h.startIdx
407
+ const end = Math.min(ops.length - 1, h.endIdx)
408
+ // Compute line numbers for header
409
+ let oldStart = 1, newStart = 1
410
+ for (let k = 0; k < start; k++) {
411
+ if (ops[k].type === 'equal' || ops[k].type === 'delete') oldStart++
412
+ if (ops[k].type === 'equal' || ops[k].type === 'insert') newStart++
413
+ }
414
+ let oldCount = 0, newCount = 0
415
+ for (let k = start; k <= end; k++) {
416
+ if (ops[k].type === 'equal' || ops[k].type === 'delete') oldCount++
417
+ if (ops[k].type === 'equal' || ops[k].type === 'insert') newCount++
418
+ }
419
+ process.stdout.write(color(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n`, 'cyan'))
420
+ for (let k = start; k <= end; k++) {
421
+ const op = ops[k]
422
+ if (op.type === 'equal') {
423
+ process.stdout.write(` ${op.line}\n`)
424
+ } else if (op.type === 'delete') {
425
+ process.stdout.write(color(`-${op.line}\n`, 'red'))
426
+ } else if (op.type === 'insert') {
427
+ process.stdout.write(color(`+${op.line}\n`, 'green'))
428
+ }
429
+ }
430
+ }
431
+ }
432
+
433
+ function loadIgnorePatterns(dir) {
434
+ const patterns = ['.git', '.vcs-next', 'node_modules', '.DS_Store', 'dist', 'build']
435
+ const tryFiles = ['.vcs-ignore', '.gitignore']
436
+ for (const f of tryFiles) {
437
+ try {
438
+ const content = fs.readFileSync(path.join(dir, f), 'utf8')
439
+ const lines = content.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'))
440
+ patterns.push(...lines)
441
+ } catch {}
442
+ }
443
+ return [...new Set(patterns)]
444
+ }
445
+
446
+ function shouldIgnore(p, patterns) {
447
+ const segments = p.split('/')
448
+ for (const pat of patterns) {
449
+ if (segments.includes(pat)) return true
450
+ if (p === pat || p.startsWith(pat + '/')) return true
451
+ // Basic glob-like support for simple cases
452
+ if (pat.startsWith('**/')) {
453
+ const sub = pat.slice(3)
454
+ if (p.endsWith('/' + sub) || p === sub) return true
455
+ }
456
+ }
457
+ return false
458
+ }
459
+
342
460
  async function collectLocal(dir) {
343
461
  const out = {}
344
462
  const base = path.resolve(dir)
463
+ const patterns = loadIgnorePatterns(base)
345
464
  async function walk(cur, rel) {
346
465
  const entries = await fs.promises.readdir(cur, { withFileTypes: true })
347
466
  for (const e of entries) {
348
- if (e.name === '.git' || e.name === '.vcs-next') continue
349
- const abs = path.join(cur, e.name)
350
467
  const rp = rel ? rel + '/' + e.name : e.name
468
+ if (shouldIgnore(rp, patterns)) continue
469
+ const abs = path.join(cur, e.name)
351
470
  if (e.isDirectory()) {
352
471
  await walk(abs, rp)
353
472
  } else if (e.isFile()) {
@@ -392,25 +511,98 @@ async function fetchRemoteFilesMap(server, repo, branch, token) {
392
511
  async function cmdStatus(opts) {
393
512
  const dir = path.resolve(opts.dir || '.')
394
513
  const meta = readRemoteMeta(dir)
395
- const cfg = loadConfig()
396
- const server = getServer(opts, cfg) || meta.server
397
- const token = getToken(opts, cfg) || meta.token
514
+ const metaDir = path.join(dir, '.vcs-next')
515
+ const localPath = path.join(metaDir, 'local.json')
516
+ let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
517
+ try {
518
+ const s = fs.readFileSync(localPath, 'utf8')
519
+ localMeta = JSON.parse(s)
520
+ } catch {}
521
+
398
522
  const local = await collectLocal(dir)
399
- const remote = await fetchRemoteFilesMap(server, meta.repoId, meta.branch, token)
400
- const added = []
401
- const modified = []
402
- const deleted = []
403
- const remotePaths = new Set(Object.keys(remote.map))
404
- const localPaths = new Set(Object.keys(local))
405
- for (const p of localPaths) {
406
- if (!remotePaths.has(p)) added.push(p)
407
- else if (remote.map[p] !== local[p].id) modified.push(p)
523
+ const baseFiles = localMeta.baseFiles || {}
524
+ const pendingFiles = localMeta.pendingCommit ? localMeta.pendingCommit.files : null
525
+
526
+ const untracked = []
527
+ const modifiedUnstaged = []
528
+ const deletedUnstaged = []
529
+ const modifiedStaged = []
530
+ const deletedStaged = []
531
+ const newStaged = []
532
+
533
+ // If there's a pending commit, that's our "staged" area
534
+ if (pendingFiles) {
535
+ // staged changes: pendingFiles vs baseFiles
536
+ const allStagedPaths = new Set([...Object.keys(baseFiles), ...Object.keys(pendingFiles)])
537
+ for (const p of allStagedPaths) {
538
+ const b = baseFiles[p]
539
+ const s = pendingFiles[p]
540
+ if (b === undefined && s !== undefined) newStaged.push(p)
541
+ else if (b !== undefined && s === undefined) deletedStaged.push(p)
542
+ else if (b !== s) modifiedStaged.push(p)
543
+ }
544
+
545
+ // unstaged changes: local vs pendingFiles
546
+ const localPaths = new Set(Object.keys(local))
547
+ const stagedPaths = new Set(Object.keys(pendingFiles))
548
+ for (const p of localPaths) {
549
+ if (!stagedPaths.has(p)) untracked.push(p)
550
+ else if (pendingFiles[p] !== local[p].content) modifiedUnstaged.push(p)
551
+ }
552
+ for (const p of stagedPaths) {
553
+ if (!localPaths.has(p)) deletedUnstaged.push(p)
554
+ }
555
+ } else {
556
+ // No pending commit: just local vs baseFiles
557
+ const localPaths = new Set(Object.keys(local))
558
+ const basePaths = new Set(Object.keys(baseFiles))
559
+ for (const p of localPaths) {
560
+ if (!basePaths.has(p)) untracked.push(p)
561
+ else if (baseFiles[p] !== local[p].content) modifiedUnstaged.push(p)
562
+ }
563
+ for (const p of basePaths) {
564
+ if (!localPaths.has(p)) deletedUnstaged.push(p)
565
+ }
408
566
  }
409
- for (const p of remotePaths) {
410
- if (!localPaths.has(p)) deleted.push(p)
567
+
568
+ if (opts.json === 'true') {
569
+ print({
570
+ branch: meta.branch,
571
+ ahead: localMeta.pendingCommit ? 1 : 0,
572
+ staged: { modified: modifiedStaged, deleted: deletedStaged, new: newStaged },
573
+ unstaged: { modified: modifiedUnstaged, deleted: deletedUnstaged },
574
+ untracked
575
+ }, true)
576
+ return
577
+ }
578
+
579
+ process.stdout.write(`On branch ${color(meta.branch, 'cyan')}\n`)
580
+ if (localMeta.pendingCommit) {
581
+ process.stdout.write(`Your branch is ahead of 'origin/${meta.branch}' by 1 commit.\n`)
582
+ }
583
+
584
+ if (modifiedStaged.length > 0 || deletedStaged.length > 0 || newStaged.length > 0) {
585
+ process.stdout.write('\nChanges to be committed:\n')
586
+ for (const p of newStaged) process.stdout.write(color(` new file: ${p}\n`, 'green'))
587
+ for (const p of modifiedStaged) process.stdout.write(color(` modified: ${p}\n`, 'green'))
588
+ for (const p of deletedStaged) process.stdout.write(color(` deleted: ${p}\n`, 'green'))
589
+ }
590
+
591
+ if (modifiedUnstaged.length > 0 || deletedUnstaged.length > 0) {
592
+ process.stdout.write('\nChanges not staged for commit:\n')
593
+ for (const p of modifiedUnstaged) process.stdout.write(color(` modified: ${p}\n`, 'red'))
594
+ for (const p of deletedUnstaged) process.stdout.write(color(` deleted: ${p}\n`, 'red'))
595
+ }
596
+
597
+ if (untracked.length > 0) {
598
+ process.stdout.write('\nUntracked files:\n')
599
+ for (const p of untracked) process.stdout.write(color(` ${p}\n`, 'red'))
600
+ }
601
+
602
+ if (modifiedStaged.length === 0 && deletedStaged.length === 0 && newStaged.length === 0 &&
603
+ modifiedUnstaged.length === 0 && deletedUnstaged.length === 0 && untracked.length === 0) {
604
+ process.stdout.write('nothing to commit, working tree clean\n')
411
605
  }
412
- const out = { branch: meta.branch, head: remote.headCommitId, added, modified, deleted }
413
- print(out, opts.json === 'true')
414
606
  }
415
607
 
416
608
  async function cmdRestore(opts) {
@@ -475,7 +667,7 @@ async function cmdDiff(opts) {
475
667
  const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commitId, token)
476
668
  const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {}, commitId: '' }
477
669
 
478
- const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)]))
670
+ const files = filePath ? [filePath] : [...new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)])]
479
671
  for (const p of files) {
480
672
  const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
481
673
  const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
@@ -483,27 +675,17 @@ async function cmdDiff(opts) {
483
675
  if (opts.json === 'true') {
484
676
  print({ path: p, old: oldContent, new: newContent }, true)
485
677
  } else {
486
- process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
678
+ process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'bold'))
487
679
  if (oldContent === null) {
488
680
  process.stdout.write(color(`+++ b/${p}\n`, 'green'))
489
- process.stdout.write(color(`+${newContent}\n`, 'green'))
681
+ const lines = newContent.split(/\r?\n/)
682
+ for (const line of lines) process.stdout.write(color(`+${line}\n`, 'green'))
490
683
  } else if (newContent === null) {
491
684
  process.stdout.write(color(`--- a/${p}\n`, 'red'))
492
- process.stdout.write(color(`-${oldContent}\n`, 'red'))
685
+ const lines = oldContent.split(/\r?\n/)
686
+ for (const line of lines) process.stdout.write(color(`-${line}\n`, 'red'))
493
687
  } else {
494
- const oldLines = oldContent.split(/\r?\n/)
495
- const newLines = newContent.split(/\r?\n/)
496
- const maxLen = Math.max(oldLines.length, newLines.length)
497
- for (let i = 0; i < maxLen; i++) {
498
- const oldLine = oldLines[i]
499
- const newLine = newLines[i]
500
- if (oldLine !== newLine) {
501
- if (oldLine !== undefined) process.stdout.write(color(`-${oldLine}\n`, 'red'))
502
- if (newLine !== undefined) process.stdout.write(color(`+${newLine}\n`, 'green'))
503
- } else if (oldLine !== undefined) {
504
- process.stdout.write(` ${oldLine}\n`)
505
- }
506
- }
688
+ printUnifiedDiff(oldContent.split(/\r?\n/), newContent.split(/\r?\n/))
507
689
  }
508
690
  }
509
691
  }
@@ -512,41 +694,37 @@ async function cmdDiff(opts) {
512
694
  }
513
695
 
514
696
  // Show diff for working directory
515
- const remoteSnap = await fetchRemoteSnapshot(server, meta.repoId, meta.branch, token)
697
+ const metaDir = path.join(dir, '.vcs-next')
698
+ const localPath = path.join(metaDir, 'local.json')
699
+ let localMeta = { baseFiles: {} }
700
+ try {
701
+ const s = fs.readFileSync(localPath, 'utf8')
702
+ localMeta = JSON.parse(s)
703
+ } catch {}
704
+
705
+ const baseFiles = localMeta.pendingCommit ? localMeta.pendingCommit.files : localMeta.baseFiles
516
706
  const local = await collectLocal(dir)
517
- const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(remoteSnap.files), ...Object.keys(local)]))
707
+ const files = filePath ? [filePath] : [...new Set([...Object.keys(baseFiles), ...Object.keys(local)])]
518
708
 
519
709
  for (const p of files) {
520
- const remoteContent = remoteSnap.files[p] !== undefined ? String(remoteSnap.files[p]) : null
710
+ const baseContent = baseFiles[p] !== undefined ? String(baseFiles[p]) : null
521
711
  const localContent = local[p]?.content || null
522
- const remoteId = remoteSnap.files[p] !== undefined ? hashContent(Buffer.from(String(remoteSnap.files[p]))) : null
523
- const localId = local[p]?.id
524
712
 
525
- if (remoteId !== localId) {
713
+ if (baseContent !== localContent) {
526
714
  if (opts.json === 'true') {
527
- print({ path: p, remote: remoteContent, local: localContent }, true)
715
+ print({ path: p, base: baseContent, local: localContent }, true)
528
716
  } else {
529
- process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
717
+ process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'bold'))
530
718
  if (localContent === null) {
531
719
  process.stdout.write(color(`--- a/${p}\n`, 'red'))
532
- process.stdout.write(color(`-${remoteContent || ''}\n`, 'red'))
533
- } else if (remoteContent === null) {
720
+ const lines = (baseContent || '').split(/\r?\n/)
721
+ for (const line of lines) process.stdout.write(color(`-${line}\n`, 'red'))
722
+ } else if (baseContent === null) {
534
723
  process.stdout.write(color(`+++ b/${p}\n`, 'green'))
535
- process.stdout.write(color(`+${localContent}\n`, 'green'))
724
+ const lines = localContent.split(/\r?\n/)
725
+ for (const line of lines) process.stdout.write(color(`+${line}\n`, 'green'))
536
726
  } else {
537
- const remoteLines = String(remoteContent).split(/\r?\n/)
538
- const localLines = String(localContent).split(/\r?\n/)
539
- const maxLen = Math.max(remoteLines.length, localLines.length)
540
- for (let i = 0; i < maxLen; i++) {
541
- const remoteLine = remoteLines[i]
542
- const localLine = localLines[i]
543
- if (remoteLine !== localLine) {
544
- if (remoteLine !== undefined) process.stdout.write(color(`-${remoteLine}\n`, 'red'))
545
- if (localLine !== undefined) process.stdout.write(color(`+${localLine}\n`, 'green'))
546
- } else if (remoteLine !== undefined) {
547
- process.stdout.write(` ${remoteLine}\n`)
548
- }
549
- }
727
+ printUnifiedDiff(baseContent.split(/\r?\n/), localContent.split(/\r?\n/))
550
728
  }
551
729
  }
552
730
  }