1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-20 09:18:48 +02:00

Implement rough content-aware inline adjustment rules for ghosts

Summary:
Ref T7447. Fixes T7600. This likely needs significant adjustment, but implements content-aware comment porting for line changes.

Specifically, this moves lines around to adjust their position considering added and removed lines between the diffs and across rebases.

It does not try to do any actual content (line against line) matching.

Test Plan:
  - Unit tests.
  - Poking around in the web UI seems to generate mostly reasonable-ish results?
  - This may be a huge step backward in some cases that I just haven't hit.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: yelirekim, epriestley

Maniphest Tasks: T7600, T7447

Differential Revision: https://secure.phabricator.com/D12741
This commit is contained in:
epriestley 2015-05-07 14:09:41 -07:00
parent 524aee03dc
commit a238f6a759
14 changed files with 953 additions and 1 deletions

View file

@ -293,6 +293,7 @@ phutil_register_library_map(array(
'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
'DifferentialActionMenuEventListener' => 'applications/differential/event/DifferentialActionMenuEventListener.php',
'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php',
'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php',
'DifferentialArcanistProjectField' => 'applications/differential/customfield/DifferentialArcanistProjectField.php',
@ -391,6 +392,7 @@ phutil_register_library_map(array(
'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.php',
'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
@ -3530,6 +3532,7 @@ phutil_register_library_map(array(
'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
'DifferentialActionMenuEventListener' => 'PhabricatorEventListener',
'DifferentialAddCommentView' => 'AphrontView',
'DifferentialAdjustmentMapTestCase' => 'ArcanistPhutilTestCase',
'DifferentialAffectedPath' => 'DifferentialDAO',
'DifferentialApplyPatchField' => 'DifferentialCustomField',
'DifferentialArcanistProjectField' => 'DifferentialCustomField',
@ -3632,6 +3635,7 @@ phutil_register_library_map(array(
'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
'DifferentialLegacyHunk' => 'DifferentialHunk',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialCustomField',
'DifferentialLocalCommitsView' => 'AphrontView',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',

View file

@ -0,0 +1,376 @@
<?php
/**
* Datastructure which follows lines of code across source changes.
*
* This map is used to update the positions of inline comments after diff
* updates. For example, if a inline comment appeared on line 30 of a diff
* but the next update adds 15 more lines above it, the comment should move
* down to line 45.
*
*/
final class DifferentialLineAdjustmentMap extends Phobject {
private $map;
private $nearestMap;
private $isInverse;
private $finalOffset;
private $nextMapInChain;
/**
* Get the raw adjustment map.
*/
public function getMap() {
return $this->map;
}
public function getNearestMap() {
if ($this->nearestMap === null) {
$this->buildNearestMap();
}
return $this->nearestMap;
}
public function getFinalOffset() {
// Make sure we've built this map already.
$this->getNearestMap();
return $this->finalOffset;
}
/**
* Add a map to the end of the chain.
*
* When a line is mapped with @{method:mapLine}, it is mapped through all
* maps in the chain.
*/
public function addMapToChain(DifferentialLineAdjustmentMap $map) {
if ($this->nextMapInChain) {
$this->nextMapInChain->addMapToChain($map);
} else {
$this->nextMapInChain = $map;
}
return $this;
}
/**
* Map a line across a change, or a series of changes.
*
* @param int Line to map
* @param bool True to map it as the end of a range.
* @return wild Spooky magic.
*/
public function mapLine($line, $is_end) {
$nmap = $this->getNearestMap();
$deleted = false;
$offset = false;
if (isset($nmap[$line])) {
$line_range = $nmap[$line];
if ($is_end) {
$to_line = end($line_range);
} else {
$to_line = reset($line_range);
}
if ($to_line <= 0) {
// If we're tracing the first line and this block is collapsing,
// compute the offset from the top of the block.
if (!$is_end && $this->isInverse) {
$offset = 0;
$cursor = $line - 1;
while (isset($nmap[$cursor])) {
$prev = $nmap[$cursor];
$prev = reset($prev);
if ($prev == $to_line) {
$offset++;
} else {
break;
}
$cursor--;
}
}
$to_line = -$to_line;
if (!$this->isInverse) {
$deleted = true;
}
}
$line = $to_line;
} else {
$line = $line + $this->finalOffset;
}
if ($this->nextMapInChain) {
$chain = $this->nextMapInChain->mapLine($line, $is_end);
list($chain_deleted, $chain_offset, $line) = $chain;
$deleted = ($deleted || $chain_deleted);
if ($chain_offset !== false) {
if ($offset === false) {
$offset = 0;
}
$offset += $chain_offset;
}
}
return array($deleted, $offset, $line);
}
/**
* Build a derived map which maps deleted lines to the nearest valid line.
*
* This computes a "nearest line" map and a final-line offset. These
* derived maps allow us to map deleted code to the previous (or next) line
* which actually exists.
*/
private function buildNearestMap() {
$map = $this->map;
$nmap = array();
$nearest = 0;
foreach ($map as $key => $value) {
if ($value) {
$nmap[$key] = $value;
$nearest = end($value);
} else {
$nmap[$key][0] = -$nearest;
}
}
if (isset($key)) {
$this->finalOffset = ($nearest - $key);
} else {
$this->finalOffset = 0;
}
foreach (array_reverse($map, true) as $key => $value) {
if ($value) {
$nearest = reset($value);
} else {
$nmap[$key][1] = -$nearest;
}
}
$this->nearestMap = $nmap;
return $this;
}
public static function newFromHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$map = array();
$o = 0;
$n = 0;
$hunks = msort($hunks, 'getOldOffset');
foreach ($hunks as $hunk) {
// If the hunks are disjoint, add the implied missing lines where
// nothing changed.
$min = ($hunk->getOldOffset() - 1);
while ($o < $min) {
$o++;
$n++;
$map[$o][] = $n;
}
$lines = $hunk->getStructuredLines();
foreach ($lines as $line) {
switch ($line['type']) {
case '-':
$o++;
$map[$o] = array();
break;
case '+':
$n++;
$map[$o][] = $n;
break;
case ' ':
$o++;
$n++;
$map[$o][] = $n;
break;
default:
break;
}
}
}
$map = self::reduceMapRanges($map);
return self::newFromMap($map);
}
public static function newFromMap(array $map) {
$obj = new DifferentialLineAdjustmentMap();
$obj->map = $map;
return $obj;
}
public static function newInverseMap(DifferentialLineAdjustmentMap $map) {
$old = $map->getMap();
$inv = array();
$last = 0;
foreach ($old as $k => $v) {
if (count($v) > 1) {
$v = range(reset($v), end($v));
}
if ($k == 0) {
foreach ($v as $line) {
$inv[$line] = array();
$last = $line;
}
} else if ($v) {
$first = true;
foreach ($v as $line) {
if ($first) {
$first = false;
$inv[$line][] = $k;
$last = $line;
} else {
$inv[$line] = array();
}
}
} else {
$inv[$last][] = $k;
}
}
$inv = self::reduceMapRanges($inv);
$obj = new DifferentialLineAdjustmentMap();
$obj->map = $inv;
$obj->isInverse = !$map->isInverse;
return $obj;
}
private static function reduceMapRanges(array $map) {
foreach ($map as $key => $values) {
if (count($values) > 2) {
$map[$key] = array(reset($values), end($values));
}
}
return $map;
}
public static function loadMaps(array $maps) {
$keys = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$keys[self::getCacheKey($u, $v)] = $map;
}
$cache = new PhabricatorKeyValueDatabaseCache();
$cache = new PhutilKeyValueCacheProfiler($cache);
$cache->setProfiler(PhutilServiceProfiler::getInstance());
$results = array();
if ($keys) {
$caches = $cache->getKeys(array_keys($keys));
foreach ($caches as $key => $value) {
list($u, $v) = $keys[$key];
try {
$results[$u][$v] = self::newFromMap(
phutil_json_decode($value));
} catch (Exception $ex) {
// Ignore, rebuild below.
}
unset($keys[$key]);
}
}
if ($keys) {
$built = self::buildMaps($maps);
$write = array();
foreach ($built as $u => $list) {
foreach ($list as $v => $map) {
$write[self::getCacheKey($u, $v)] = json_encode($map->getMap());
$results[$u][$v] = $map;
}
}
$cache->setKeys($write);
}
return $results;
}
private static function buildMaps(array $maps) {
$need = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$need[$u] = $u;
$need[$v] = $v;
}
if ($need) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs($need)
->needHunks(true)
->execute();
$changesets = mpull($changesets, null, 'getID');
}
$results = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$u_set = idx($changesets, $u);
$v_set = idx($changesets, $v);
if (!$u_set || !$v_set) {
continue;
}
// This is the simple case.
if ($u == $v) {
$results[$u][$v] = self::newFromHunks(
$u_set->getHunks());
continue;
}
$u_old = $u_set->makeOldFile();
$v_old = $v_set->makeOldFile();
// No difference between the two left sides.
if ($u_old == $v_old) {
$results[$u][$v] = self::newFromMap(
array());
continue;
}
// If we're missing context, this won't currently work. We can
// make this case work, but it's fairly rare.
$u_hunks = $u_set->getHunks();
$v_hunks = $v_set->getHunks();
if (count($u_hunks) != 1 ||
count($v_hunks) != 1 ||
head($u_hunks)->getOldOffset() != 1 ||
head($u_hunks)->getNewOffset() != 1 ||
head($v_hunks)->getOldOffset() != 1 ||
head($v_hunks)->getNewOffset() != 1) {
continue;
}
$changeset = id(new PhabricatorDifferenceEngine())
->setIgnoreWhitespace(true)
->generateChangesetFromFileContent($u_old, $v_old);
$results[$u][$v] = self::newFromHunks(
$changeset->getHunks());
}
return $results;
}
private static function getCacheKey($u, $v) {
return 'diffadjust.v1('.$u.','.$v.')';
}
}

View file

@ -323,6 +323,7 @@ final class DifferentialInlineCommentQuery
'new' => $is_new,
'reason' => $reason,
'href' => $href,
'originalID' => $changeset->getID(),
));
$results[] = $inline;
@ -348,6 +349,107 @@ final class DifferentialInlineCommentQuery
}
}
// Adjust inline line numbers to account for content changes across
// updates and rebases.
$plan = array();
$need = array();
foreach ($results as $inline) {
$ghost = $inline->getIsGhost();
if (!$ghost) {
// If this isn't a "ghost" inline, ignore it.
continue;
}
$src_id = $ghost['originalID'];
$dst_id = $inline->getChangesetID();
$xforms = array();
// If the comment is on the right, transform it through the inverse map
// back to the left.
if ($inline->getIsNewFile()) {
$xforms[] = array($src_id, $src_id, true);
}
// Transform it across rebases.
$xforms[] = array($src_id, $dst_id, false);
// If the comment is on the right, transform it back onto the right.
if ($inline->getIsNewFile()) {
$xforms[] = array($dst_id, $dst_id, false);
}
$key = array();
foreach ($xforms as $xform) {
list($u, $v, $inverse) = $xform;
$short = $u.'/'.$v;
$need[$short] = array($u, $v);
$part = $u.($inverse ? '<' : '>').$v;
$key[] = $part;
}
$key = implode(',', $key);
if (empty($plan[$key])) {
$plan[$key] = array(
'xforms' => $xforms,
'inlines' => array(),
);
}
$plan[$key]['inlines'][] = $inline;
}
if ($need) {
$maps = DifferentialLineAdjustmentMap::loadMaps($need);
} else {
$maps = array();
}
foreach ($plan as $step) {
$xforms = $step['xforms'];
$chain = null;
foreach ($xforms as $xform) {
list($u, $v, $inverse) = $xform;
$map = idx(idx($maps, $u, array()), $v);
if (!$map) {
continue 2;
}
if ($inverse) {
$map = DifferentialLineAdjustmentMap::newInverseMap($map);
} else {
$map = clone $map;
}
if ($chain) {
$chain->addMapToChain($map);
} else {
$chain = $map;
}
}
foreach ($step['inlines'] as $inline) {
$head_line = $inline->getLineNumber();
$tail_line = ($head_line + $inline->getLineLength());
$head_info = $chain->mapLine($head_line, false);
$tail_info = $chain->mapLine($tail_line, true);
list($head_deleted, $head_offset, $head_line) = $head_info;
list($tail_deleted, $tail_offset, $tail_line) = $tail_info;
if ($head_offset !== false) {
$inline->setLineNumber($head_line + 1 + $head_offset);
} else {
$inline->setLineNumber($head_line);
$inline->setLineLength($tail_line - $head_line);
}
}
}
return $results;
}

View file

@ -278,6 +278,7 @@ final class DifferentialChangesetTwoUpRenderer
$scaffold->addInlineView($companion);
unset($new_comments[$n_num][$key]);
break;
}
}
}

View file

@ -117,7 +117,7 @@ abstract class DifferentialHunk extends DifferentialDAO
return $this->splitLines;
}
private function getStructuredLines() {
public function getStructuredLines() {
if ($this->structuredLines === null) {
$lines = $this->getSplitLines();

View file

@ -0,0 +1,294 @@
<?php
final class DifferentialAdjustmentMapTestCase extends ArcanistPhutilTestCase {
public function testBasicMaps() {
$change_map = array(
1 => array(1),
2 => array(2),
3 => array(3),
4 => array(),
5 => array(),
6 => array(),
7 => array(4),
8 => array(5),
9 => array(6),
10 => array(7),
11 => array(8),
12 => array(9),
13 => array(10),
14 => array(11),
15 => array(12),
16 => array(13),
17 => array(14),
18 => array(15),
19 => array(16),
20 => array(17, 20),
21 => array(21),
22 => array(22),
23 => array(23),
24 => array(24),
25 => array(25),
26 => array(26),
);
$hunks = $this->loadHunks('add.diff');
$this->assertEqual(
array(
0 => array(1, 26),
),
DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
$hunks = $this->loadHunks('change.diff');
$this->assertEqual(
$change_map,
DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
$hunks = $this->loadHunks('remove.diff');
$this->assertEqual(
array_fill_keys(range(1, 26), array()),
DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
// With the contextless diff, we don't get the last few similar lines
// in the map.
$reduced_map = $change_map;
unset($reduced_map[24]);
unset($reduced_map[25]);
unset($reduced_map[26]);
$hunks = $this->loadHunks('context.diff');
$this->assertEqual(
$reduced_map,
DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
}
public function testInverseMaps() {
$change_map = array(
1 => array(1),
2 => array(2),
3 => array(3, 6),
4 => array(7),
5 => array(8),
6 => array(9),
7 => array(10),
8 => array(11),
9 => array(12),
10 => array(13),
11 => array(14),
12 => array(15),
13 => array(16),
14 => array(17),
15 => array(18),
16 => array(19),
17 => array(20),
18 => array(),
19 => array(),
20 => array(),
21 => array(21),
22 => array(22),
23 => array(23),
24 => array(24),
25 => array(25),
26 => array(26),
);
$hunks = $this->loadHunks('add.diff');
$this->assertEqual(
array_fill_keys(range(1, 26), array()),
DifferentialLineAdjustmentMap::newInverseMap(
DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
$hunks = $this->loadHunks('change.diff');
$this->assertEqual(
$change_map,
DifferentialLineAdjustmentMap::newInverseMap(
DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
$hunks = $this->loadHunks('remove.diff');
$this->assertEqual(
array(
0 => array(1, 26),
),
DifferentialLineAdjustmentMap::newInverseMap(
DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
// With the contextless diff, we don't get the last few similar lines
// in the map.
$reduced_map = $change_map;
unset($reduced_map[24]);
unset($reduced_map[25]);
unset($reduced_map[26]);
$hunks = $this->loadHunks('context.diff');
$this->assertEqual(
$reduced_map,
DifferentialLineAdjustmentMap::newInverseMap(
DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
}
public function testNearestMaps() {
$change_map = array(
1 => array(1),
2 => array(2),
3 => array(3),
4 => array(-3, -4),
5 => array(-3, -4),
6 => array(-3, -4),
7 => array(4),
8 => array(5),
9 => array(6),
10 => array(7),
11 => array(8),
12 => array(9),
13 => array(10),
14 => array(11),
15 => array(12),
16 => array(13),
17 => array(14),
18 => array(15),
19 => array(16),
20 => array(17, 20),
21 => array(21),
22 => array(22),
23 => array(23),
24 => array(24),
25 => array(25),
26 => array(26),
);
$hunks = $this->loadHunks('add.diff');
$map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
$this->assertEqual(
array(
0 => array(1, 26),
),
$map->getNearestMap());
$this->assertEqual(26, $map->getFinalOffset());
$hunks = $this->loadHunks('change.diff');
$map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
$this->assertEqual(
$change_map,
$map->getNearestMap());
$this->assertEqual(0, $map->getFinalOffset());
$hunks = $this->loadHunks('remove.diff');
$map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
$this->assertEqual(
array_fill_keys(
range(1, 26),
array(0, 0)),
$map->getNearestMap());
$this->assertEqual(-26, $map->getFinalOffset());
$reduced_map = $change_map;
unset($reduced_map[24]);
unset($reduced_map[25]);
unset($reduced_map[26]);
$hunks = $this->loadHunks('context.diff');
$map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
$this->assertEqual(
$reduced_map,
$map->getNearestMap());
$this->assertEqual(0, $map->getFinalOffset());
$hunks = $this->loadHunks('insert.diff');
$map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
$this->assertEqual(
array(
1 => array(1),
2 => array(2),
3 => array(3),
4 => array(4),
5 => array(5),
6 => array(6),
7 => array(7),
8 => array(8),
9 => array(9),
10 => array(10, 13),
11 => array(14),
12 => array(15),
13 => array(16),
),
$map->getNearestMap());
$this->assertEqual(3, $map->getFinalOffset());
}
public function testChainMaps() {
// This test simulates porting inlines forward across a rebase.
// Part 1 is the original diff.
// Part 2 is the rebase, which we would normally compute synthetically.
// Part 3 is the updated diff against the rebased changes.
$diff1 = $this->loadHunks('chain.adjust.1.diff');
$diff2 = $this->loadHunks('chain.adjust.2.diff');
$diff3 = $this->loadHunks('chain.adjust.3.diff');
$map = DifferentialLineAdjustmentMap::newInverseMap(
DifferentialLineAdjustmentMap::newFromHunks($diff1));
$map->addMapToChain(
DifferentialLineAdjustmentMap::newFromHunks($diff2));
$map->addMapToChain(
DifferentialLineAdjustmentMap::newFromHunks($diff3));
$actual = array();
for ($ii = 1; $ii <= 13; $ii++) {
$actual[$ii] = array(
$map->mapLine($ii, false),
$map->mapLine($ii, true),
);
}
$this->assertEqual(
array(
1 => array(array(false, false, 1), array(false, false, 1)),
2 => array(array(true, false, 1), array(true, false, 2)),
3 => array(array(true, false, 1), array(true, false, 2)),
4 => array(array(false, false, 2), array(false, false, 2)),
5 => array(array(false, false, 3), array(false, false, 3)),
6 => array(array(false, false, 4), array(false, false, 4)),
7 => array(array(false, false, 5), array(false, false, 8)),
8 => array(array(false, 0, 5), array(false, false, 9)),
9 => array(array(false, 1, 5), array(false, false, 9)),
10 => array(array(false, 2, 5), array(false, false, 9)),
11 => array(array(false, false, 9), array(false, false, 9)),
12 => array(array(false, false, 10), array(false, false, 10)),
13 => array(array(false, false, 11), array(false, false, 11)),
),
$actual);
}
private function loadHunks($name) {
$root = dirname(__FILE__).'/map/';
$data = Filesystem::readFile($root.$name);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
$viewer = PhabricatorUser::getOmnipotentUser();
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes);
$changesets = $diff->getChangesets();
if (count($changesets) !== 1) {
throw new Exception(
pht(
'Expected exactly one changeset from "%s".',
$name));
}
$changeset = head($changesets);
return $changeset->getHunks();
}
}

View file

@ -0,0 +1,32 @@
diff --git a/alphabet b/alphabet
new file mode 100644
index 0000000..0edb856
--- /dev/null
+++ b/alphabet
@@ -0,0 +1,26 @@
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x
+y
+z

View file

@ -0,0 +1,14 @@
diff --git a/alphabet b/alphabet
index 92dfa21..292798b 100644
--- a/alphabet
+++ b/alphabet
@@ -5,6 +5,9 @@ d
e
f
g
+G1
+G2
+G3
h
i
j

View file

@ -0,0 +1,11 @@
diff --git a/alphabet b/alphabet
index 92dfa21..e3344af 100644
--- a/alphabet
+++ b/alphabet
@@ -1,6 +1,4 @@
a
-b
-c
d
e
f

View file

@ -0,0 +1,14 @@
diff --git a/alphabet b/alphabet
index e3344af..febfe3e 100644
--- a/alphabet
+++ b/alphabet
@@ -3,6 +3,9 @@ d
e
f
g
+G1x
+G2x
+G3x
h
i
j

View file

@ -0,0 +1,34 @@
diff --git a/alphabet b/alphabet
index 0edb856..2449de2 100644
--- a/alphabet
+++ b/alphabet
@@ -1,26 +1,26 @@
a
b
c
-d
-e
-f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
+tx
+ty
+tz
u
v
w
x
y
z

View file

@ -0,0 +1,24 @@
diff --git a/alphabet b/alphabet
index 0edb856..2449de2 100644
--- a/alphabet
+++ b/alphabet
@@ -1,9 +1,6 @@
a
b
c
-d
-e
-f
g
h
i
@@ -18,6 +15,9 @@ q
r
s
t
+tx
+ty
+tz
u
v
w

View file

@ -0,0 +1,14 @@
diff --git a/alphabet b/alphabet
index f2b41ef..755b349 100644
--- a/alphabet
+++ b/alphabet
@@ -8,6 +8,9 @@ g
h
i
j
+j1
+j2
+j3
k
l
n

View file

@ -0,0 +1,32 @@
diff --git a/alphabet b/alphabet
deleted file mode 100644
index 2449de2..0000000
--- a/alphabet
+++ /dev/null
@@ -1,26 +0,0 @@
-a
-b
-c
-g
-h
-i
-j
-k
-l
-m
-n
-o
-p
-q
-r
-s
-t
-tx
-ty
-tz
-u
-v
-w
-x
-y
-z