From e9e06ca627d328fb67771949e36924f73b0067c9 Mon Sep 17 00:00:00 2001
From: Douwe Maan <douwe@selenight.nl>
Date: Mon, 20 Jun 2016 19:17:56 +0200
Subject: [PATCH] Add Gitlab::Diff::LineMapper

---
 lib/gitlab/diff/line_mapper.rb           |  64 +++++++++++
 spec/lib/gitlab/diff/line_mapper_spec.rb | 137 +++++++++++++++++++++++
 2 files changed, 201 insertions(+)
 create mode 100644 lib/gitlab/diff/line_mapper.rb
 create mode 100644 spec/lib/gitlab/diff/line_mapper_spec.rb

diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb
new file mode 100644
index 00000000000..bde5b4eedaa
--- /dev/null
+++ b/lib/gitlab/diff/line_mapper.rb
@@ -0,0 +1,64 @@
+# When provided a diff for a specific file, maps old line numbers to new line
+# numbers and back, to find out where a specific line in a file was moved by the
+# changes.
+module Gitlab
+  module Diff
+    class LineMapper
+      attr_accessor :diff_file
+
+      def initialize(diff_file)
+        @diff_file = diff_file
+      end
+
+      # Find new line number for old line number.
+      def old_to_new(old_line)
+        map_line_number(old_line, from: :old_line, to: :new_line)
+      end
+
+      # Find old line number for new line number.
+      def new_to_old(new_line)
+        map_line_number(new_line, from: :new_line, to: :old_line)
+      end
+
+      private
+
+      def diff_lines
+        @diff_lines ||= @diff_file.diff_lines
+      end
+
+      # Find old line number based on new line number.
+      def map_line_number(from_line, from:, to:)
+        # If no diff file could be found, the file wasn't changed, and the
+        # mapped line number is the same as the specified line number.
+        return from_line unless diff_file
+
+        # To find the mapped line number for the specified line number,
+        # we need to find:
+        # - The diff line with that exact line number, if it is in the diff context
+        # - The first diff line with a higher line number, if it falls between diff contexts
+        # - The last known diff line, if it falls after the last diff context
+        diff_line = diff_lines.find do |diff_line|
+          diff_from_line = diff_line.send(from)
+          diff_from_line && diff_from_line >= from_line
+        end
+        diff_line ||= diff_lines.last
+
+        # If no diff line could be found, the file wasn't changed, and the
+        # mapped line number is the same as the specified line number.
+        return from_line unless diff_line
+
+        diff_from_line = diff_line.send(from)
+        diff_to_line = diff_line.send(to)
+
+        # If the line was removed, there is no mapped line number.
+        return unless diff_to_line
+
+        # Because we may not have the diff line with the exact line number
+        # we were looking for, we need to adjust the mapped line number.
+        distance = diff_from_line - from_line
+
+        diff_to_line - distance
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
new file mode 100644
index 00000000000..4e50e03bb7e
--- /dev/null
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::LineMapper, lib: true do
+  include RepoHelpers
+
+  let(:project) { create(:project) }
+  let(:repository) { project.repository }
+  let(:commit) { project.commit(sample_commit.id) }
+  let(:diffs) { commit.diffs }
+  let(:diff) { diffs.first }
+  let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
+  subject { described_class.new(diff_file) }
+
+  describe '#old_to_new' do
+    context "with a diff file" do
+      let(:mapping) do
+        {
+          1 => 1,
+          2 => 2,
+          3 => 3,
+          4 => 4,
+          5 => 5,
+          6 => 6,
+          7 => 7,
+          8 => 8,
+          9 => nil,
+          # nil => 9,
+          10 => 10,
+          11 => 11,
+          12 => 12,
+          13 => nil,
+          14 => nil,
+          # nil => 15,
+          # nil => 16,
+          # nil => 17,
+          # nil => 18,
+          # nil => 19,
+          # nil => 20,
+          15 => 21,
+          16 => 22,
+          17 => 23,
+          18 => 24,
+          19 => 25,
+          20 => 26,
+          21 => 27,
+          # nil => 28,
+          22 => 29,
+          23 => 30,
+          24 => 31,
+          25 => 32,
+          26 => 33,
+          27 => 34,
+          28 => 35,
+          29 => 36,
+          30 => 37
+        }
+      end
+
+      it 'returns the new line number for the old line number' do
+        mapping.each do |old_line, new_line|
+          expect(subject.old_to_new(old_line)).to eq(new_line)
+        end
+      end
+    end
+
+    context "without a diff file" do
+      let(:diff_file) { nil }
+
+      it "returns the same line number" do
+        expect(subject.old_to_new(100)).to eq(100)
+      end
+    end
+  end
+
+  describe '#new_to_old' do
+    context "with a diff file" do
+      let(:mapping) do
+        {
+          1 => 1,
+          2 => 2,
+          3 => 3,
+          4 => 4,
+          5 => 5,
+          6 => 6,
+          7 => 7,
+          8 => 8,
+          # nil => 9,
+          9 => nil,
+          10 => 10,
+          11 => 11,
+          12 => 12,
+          # nil => 13,
+          # nil => 14,
+          13 => nil,
+          14 => nil,
+          15 => nil,
+          16 => nil,
+          17 => nil,
+          18 => nil,
+          19 => nil,
+          20 => nil,
+          21 => 15,
+          22 => 16,
+          23 => 17,
+          24 => 18,
+          25 => 19,
+          26 => 20,
+          27 => 21,
+          28 => nil,
+          29 => 22,
+          30 => 23,
+          31 => 24,
+          32 => 25,
+          33 => 26,
+          34 => 27,
+          35 => 28,
+          36 => 29,
+          37 => 30
+        }
+      end
+
+      it 'returns the old line number for the new line number' do
+        mapping.each do |new_line, old_line|
+          expect(subject.new_to_old(new_line)).to eq(old_line)
+        end
+      end
+    end
+
+    context "without a diff file" do
+      let(:diff_file) { nil }
+
+      it "returns the same line number" do
+        expect(subject.new_to_old(100)).to eq(100)
+      end
+    end
+  end
+end
-- 
GitLab