Commit 0d0f8a3b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'feature/custom-highlighting' into 'master'

Add custom highlighting via .gitattributes

## What does this MR do?
Allows user control of language selection via a `gitlab-language` entry in `.gitattributes`

## Are there points in the code the reviewer needs to double check?
(paired with @stanhu)

## Why was this MR needed?
Guessing languages by filename is fraught and often wrong. In one project, `foo.pl` may be perl, and in another it may be prolog. Users might have a Thingfile that needs ruby highlighting, or depend on things that can't work in general, like `*.C` (capitalized) mapping to C++ instead of C.

This allows the user to override language choice so they never have to look at a mis-highlighted file.

## What are the relevant issue numbers?
https://github.com/jneen/rouge/issues/494
https://gitlab.com/gitlab-org/gitlab-ce/issues/13818 (*.tpl can't in general map to Smarty)
https://gitlab.com/gitlab-org/gitlab-ce/issues/13615 (in cases we don't have a language and mis-identify it, users could map to 'text' to turn off highlighting)

## Screenshots (if relevant)

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added (N/A)
- [x] Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

See merge request !4606
parents c9a46263 ffd81621
module BlobHelper module BlobHelper
def highlighter(blob_name, blob_content, nowrap: false) def highlighter(blob_name, blob_content, repository: nil, nowrap: false)
Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap) Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap, repository: repository)
end end
def highlight(blob_name, blob_content, nowrap: false, plain: false) def highlight(blob_name, blob_content, repository: nil, nowrap: false, plain: false)
Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain) Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain, repository: repository)
end end
def no_highlight_files def no_highlight_files
......
...@@ -978,6 +978,10 @@ class Repository ...@@ -978,6 +978,10 @@ class Repository
raw_repository.ls_files(actual_ref) raw_repository.ls_files(actual_ref)
end end
def gitattribute(path, name)
raw_repository.attributes(path)[name]
end
def copy_gitattributes(ref) def copy_gitattributes(ref)
actual_ref = ref || root_ref actual_ref = ref || root_ref
begin begin
......
...@@ -20,6 +20,7 @@ class Snippet < ActiveRecord::Base ...@@ -20,6 +20,7 @@ class Snippet < ActiveRecord::Base
length: { within: 0..255 }, length: { within: 0..255 },
format: { with: Gitlab::Regex.file_name_regex, format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message } message: Gitlab::Regex.file_name_regex_message }
validates :content, presence: true validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
...@@ -81,6 +82,11 @@ class Snippet < ActiveRecord::Base ...@@ -81,6 +82,11 @@ class Snippet < ActiveRecord::Base
0 0
end end
# alias for compatibility with blobs and highlighting
def path
file_name
end
def name def name
file_name file_name
end end
......
...@@ -16,4 +16,4 @@ ...@@ -16,4 +16,4 @@
.file-content.code .file-content.code
.nothing-here-block Empty file .nothing-here-block Empty file
- else - else
= render 'shared/file_highlight', blob: blob = render 'shared/file_highlight', blob: blob, repository: @repository
- repository = nil unless local_assigns.key?(:repository)
.file-content.code.js-syntax-highlight .file-content.code.js-syntax-highlight
.line-numbers .line-numbers
- if blob.data.present? - if blob.data.present?
...@@ -11,4 +13,4 @@ ...@@ -11,4 +13,4 @@
= link_icon = link_icon
= i = i
.blob-content{data: {blob_id: blob.id}} .blob-content{data: {blob_id: blob.id}}
= highlight(blob.name, blob.data, plain: blob.no_highlighting?) = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
[Rouge]: https://rubygems.org/gems/rouge
# Syntax Highlighting
GitLab provides syntax highlighting on all files and snippets through the [Rouge][] rubygem. It will try to guess what language to use based on the file extension, which most of the time is sufficient.
If GitLab is guessing wrong, you can override its choice of language using the `gitlab-language` attribute in `.gitattributes`. For example, if you are working in a Prolog project and using the `.pl` file extension (which would normally be highlighted as Perl), you can add the following to your `.gitattributes` file:
``` conf
*.pl gitlab-language=prolog
```
When you check in and push that change, all `*.pl` files in your project will be highlighted as Prolog.
The paths here are simply git's builtin [`.gitattributes` interface](https://git-scm.com/docs/gitattributes). So, if you were to invent a file format called a `Nicefile` at the root of your project that used ruby syntax, all you need is:
``` conf
/Nicefile gitlab-language=ruby
```
To disable highlighting entirely, use `gitlab-language=text`. Lots more fun shenanigans are available through CGI options, such as:
``` conf
# json with erb in it
/my-cool-file gitlab-language=erb?parent=json
# an entire file of highlighting errors!
/other-file gitlab-language=text?token=Error
```
Please note that these configurations will only take effect when the `.gitattributes` file is in your default branch (usually `master`).
...@@ -41,7 +41,8 @@ module Gitlab ...@@ -41,7 +41,8 @@ module Gitlab
def highlighted_lines def highlighted_lines
@blob.load_all_data!(repository) @blob.load_all_data!(repository)
@highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines @highlighted_lines ||=
Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines
end end
def project def project
......
module Gitlab module Gitlab
class Highlight class Highlight
def self.highlight(blob_name, blob_content, nowrap: true, plain: false) def self.highlight(blob_name, blob_content, repository: nil, nowrap: true, plain: false)
new(blob_name, blob_content, nowrap: nowrap). new(blob_name, blob_content, nowrap: nowrap, repository: repository).
highlight(blob_content, continue: false, plain: plain) highlight(blob_content, continue: false, plain: plain)
end end
...@@ -10,12 +10,21 @@ module Gitlab ...@@ -10,12 +10,21 @@ module Gitlab
return [] unless blob return [] unless blob
blob.load_all_data!(repository) blob.load_all_data!(repository)
highlight(file_name, blob.data).lines.map!(&:html_safe) highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
end end
def initialize(blob_name, blob_content, nowrap: true) attr_reader :lexer
def initialize(blob_name, blob_content, repository: nil, nowrap: true)
@blob_name = blob_name
@blob_content = blob_content
@repository = repository
@formatter = rouge_formatter(nowrap: nowrap) @formatter = rouge_formatter(nowrap: nowrap)
@lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText
@lexer = custom_language || begin
Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
rescue Rouge::Lexer::AmbiguousGuess => e
e.alternatives.sort_by(&:tag).first
end
end end
def highlight(text, continue: true, plain: false) def highlight(text, continue: true, plain: false)
...@@ -30,6 +39,14 @@ module Gitlab ...@@ -30,6 +39,14 @@ module Gitlab
private private
def custom_language
language_name = @repository && @repository.gitattribute(@blob_name, 'gitlab-language')
return nil unless language_name
Rouge::Lexer.find_fancy(language_name)
end
def rouge_formatter(options = {}) def rouge_formatter(options = {})
options = options.reverse_merge( options = options.reverse_merge(
nowrap: true, nowrap: true,
......
...@@ -4,6 +4,7 @@ describe Gitlab::Highlight, lib: true do ...@@ -4,6 +4,7 @@ describe Gitlab::Highlight, lib: true do
include RepoHelpers include RepoHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) } let(:commit) { project.commit(sample_commit.id) }
describe '.highlight_lines' do describe '.highlight_lines' do
...@@ -18,4 +19,30 @@ describe Gitlab::Highlight, lib: true do ...@@ -18,4 +19,30 @@ describe Gitlab::Highlight, lib: true do
end end
end end
describe 'custom highlighting from .gitattributes' do
let(:branch) { 'gitattributes' }
let(:blob) { repository.blob_at_branch(branch, path) }
let(:highlighter) do
Gitlab::Highlight.new(blob.path, blob.data, repository: repository)
end
before { project.change_head('gitattributes') }
describe 'basic language selection' do
let(:path) { 'custom-highlighting/test.gitlab-custom' }
it 'highlights as ruby' do
expect(highlighter.lexer.tag).to eq 'ruby'
end
end
describe 'cgi options' do
let(:path) { 'custom-highlighting/test.gitlab-cgi' }
it 'highlights as json with erb' do
expect(highlighter.lexer.tag).to eq 'erb'
expect(highlighter.lexer.parent.tag).to eq 'json'
end
end
end
end end
...@@ -17,6 +17,7 @@ module TestEnv ...@@ -17,6 +17,7 @@ module TestEnv
"'test'" => 'e56497b', "'test'" => 'e56497b',
'orphaned-branch' => '45127a9', 'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43', 'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481',
} }
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment