Commit bd5dbe14 authored by randx's avatar randx

Merge branch 'web_editor'

parents c01b0fd5 809aefb8
...@@ -18,4 +18,5 @@ ...@@ -18,4 +18,5 @@
//= require chosen-jquery //= require chosen-jquery
//= require raphael //= require raphael
//= require branch-graph //= require branch-graph
//= require ace-src-noconflict/ace.js
//= require_tree . //= require_tree .
...@@ -182,3 +182,9 @@ $hover: #D9EDF7; ...@@ -182,3 +182,9 @@ $hover: #D9EDF7;
* *
*/ */
@import "highlight/dark.scss"; @import "highlight/dark.scss";
/**
* File Editor styles
*
*/
@import "sections/editor.scss";
.file-editor {
#editor{
height: 500px;
width: 100%;
position: relative;
}
.editor-commit-comment {
padding-top:20px;
textarea {
width: 50%;
margin-left: 20px;
}
}
}
...@@ -59,3 +59,11 @@ ...@@ -59,3 +59,11 @@
} }
} }
} }
.tree-btn-group {
.btn {
margin-right:-3px;
padding:2px 10px;
}
}
...@@ -19,4 +19,25 @@ class TreeController < ProjectResourceController ...@@ -19,4 +19,25 @@ class TreeController < ProjectResourceController
format.js { no_cache_headers } format.js { no_cache_headers }
end end
end end
def edit
@last_commit = @project.last_commit_for(@ref, @path).sha
end
def update
file_editor = Gitlab::FileEditor.new(current_user, @project, @ref)
update_status = file_editor.update(
@path,
params[:content],
params[:commit_message],
params[:last_commit]
)
if update_status
redirect_to project_tree_path(@project, @id), :notice => "File has been successfully changed"
else
flash[:notice] = "You can't save file because it has been changed"
render :edit
end
end
end end
...@@ -23,7 +23,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -23,7 +23,7 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches validate :validate_branches
def self.find_all_by_branch(branch_name) def self.find_all_by_branch(branch_name)
where("source_branch like :branch or target_branch like :branch", branch: branch_name) where("source_branch LIKE :branch OR target_branch LIKE :branch", branch: branch_name)
end end
def human_state def human_state
......
...@@ -32,6 +32,10 @@ module Repository ...@@ -32,6 +32,10 @@ module Repository
Commit.commits(repo, ref, path, limit, offset) Commit.commits(repo, ref, path, limit, offset)
end end
def last_commit_for(ref, path = nil)
commits(ref, path, 1).first
end
def commits_between(from, to) def commits_between(from, to)
Commit.commits_between(repo, from, to) Commit.commits_between(repo, from, to)
end end
......
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
= tree_file.name.force_encoding('utf-8') = tree_file.name.force_encoding('utf-8')
%small #{tree_file.mode} %small #{tree_file.mode}
%span.options %span.options
.btn-group.tree-btn-group
= link_to "raw", project_blob_path(@project, @id), class: "btn very_small", target: "_blank" = link_to "raw", project_blob_path(@project, @id), class: "btn very_small", target: "_blank"
= link_to "history", project_commits_path(@project, @id), class: "btn very_small" = link_to "history", project_commits_path(@project, @id), class: "btn very_small"
= link_to "blame", project_blame_path(@project, @id), class: "btn very_small" = link_to "blame", project_blame_path(@project, @id), class: "btn very_small"
= link_to "edit", edit_project_tree_path(@project, @id), class: "btn very_small"
- if tree_file.text? - if tree_file.text?
- if gitlab_markdown?(tree_file.name) - if gitlab_markdown?(tree_file.name)
.file_content.wiki .file_content.wiki
......
.file-editor
= form_tag(project_tree_path(@project, @id), :method => :put) do
.file_holder
.file_title
%i.icon-file
%span.file_name
= @tree.path.force_encoding('utf-8')
%span.options
= link_to "cancel editing", project_tree_path(@project, @id), class: "btn very_small"
.file_content.code
#editor= @tree.data
.editor-commit-comment
= label_tag 'commit_message' do
%p.slead Commit message
= text_area_tag 'commit_message', '', :required => true
.form-actions
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', :id => :file_content
= button_tag "Save", class: 'btn save-btn'
:javascript
var editor = ace.edit("editor");
editor.setTheme("ace/theme/twilight");
editor.getSession().setMode("ace/mode/javascript");
$(".save-btn").click(function(){
$("#file_content").val(editor.getValue());
$(".form_editor form").submit();
});
...@@ -183,7 +183,7 @@ Gitlab::Application.routes.draw do ...@@ -183,7 +183,7 @@ Gitlab::Application.routes.draw do
resources :compare, only: [:index, :create] resources :compare, only: [:index, :create]
resources :blame, only: [:show], constraints: {id: /.+/} resources :blame, only: [:show], constraints: {id: /.+/}
resources :blob, only: [:show], constraints: {id: /.+/} resources :blob, only: [:show], constraints: {id: /.+/}
resources :tree, only: [:show], constraints: {id: /.+/} resources :tree, only: [:show, :edit, :update], constraints: {id: /.+/}
match "/compare/:from...:to" => "compare#show", as: "compare", match "/compare/:from...:to" => "compare#show", as: "compare",
:via => [:get, :post], constraints: {from: /.+/, to: /.+/} :via => [:get, :post], constraints: {from: /.+/, to: /.+/}
......
...@@ -19,3 +19,9 @@ Feature: Project Browse files ...@@ -19,3 +19,9 @@ Feature: Project Browse files
Given I visit blob file from repo Given I visit blob file from repo
And I click link "raw" And I click link "raw"
Then I should see raw file content Then I should see raw file content
@javascript
Scenario: I can edit file
Given I click on "Gemfile" file in repo
And I click button "edit"
Then I can edit code
...@@ -31,4 +31,14 @@ class ProjectBrowseFiles < Spinach::FeatureSteps ...@@ -31,4 +31,14 @@ class ProjectBrowseFiles < Spinach::FeatureSteps
Then 'I should see raw file content' do Then 'I should see raw file content' do
page.source.should == ValidCommit::BLOB_FILE page.source.should == ValidCommit::BLOB_FILE
end end
Given 'I click button "edit"' do
click_link 'edit'
end
Then 'I can edit code' do
page.execute_script('editor.setValue("GitlabFileEditor")')
page.evaluate_script('editor.getValue()').should == "GitlabFileEditor"
end
end end
module Gitlab
# GitLab file editor
#
# It gives you ability to make changes to files
# & commit this changes from GitLab UI.
class FileEditor
attr_accessor :user, :project, :ref
def initialize(user, project, ref)
self.user = user
self.project = project
self.ref = ref
end
def update(path, content, commit_message, last_commit)
return false unless can_edit?(path, last_commit)
Grit::Git.with_timeout(10.seconds) do
lock_file = Rails.root.join("tmp", "#{project.path}.lock")
File.open(lock_file, "w+") do |f|
f.flock(File::LOCK_EX)
unless project.satellite.exists?
raise "Satellite doesn't exist"
end
project.satellite.clear
Dir.chdir(project.satellite.path) do
r = Grit::Repo.new('.')
r.git.sh "git reset --hard"
r.git.sh "git fetch origin"
r.git.sh "git config user.name \"#{user.name}\""
r.git.sh "git config user.email \"#{user.email}\""
r.git.sh "git checkout -b #{ref} origin/#{ref}"
File.open(path, 'w'){|f| f.write(content)}
r.git.sh "git add ."
r.git.sh "git commit -am '#{commit_message}'"
output = r.git.sh "git push origin #{ref}"
if output =~ /reject/
return false
end
end
end
end
true
end
protected
def can_edit?(path, last_commit)
current_last_commit = @project.commits(ref, path, 1).first.sha
last_commit == current_last_commit
end
end
end
...@@ -28,13 +28,13 @@ module Gitlab ...@@ -28,13 +28,13 @@ module Gitlab
def process def process
Grit::Git.with_timeout(30.seconds) do Grit::Git.with_timeout(30.seconds) do
lock_file = Rails.root.join("tmp", "merge_repo_#{project.path}.lock") lock_file = Rails.root.join("tmp", "#{project.path}.lock")
File.open(lock_file, "w+") do |f| File.open(lock_file, "w+") do |f|
f.flock(File::LOCK_EX) f.flock(File::LOCK_EX)
unless project.satellite.exists? unless project.satellite.exists?
raise "You should run: rake gitlab:app:enable_automerge" raise "Satellite doesn't exist"
end end
project.satellite.clear project.satellite.clear
......
This diff is collapsed.
This diff is collapsed.
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2010, Ajax.org B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Ajax.org B.V. nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
ace.define('ace/mode/c9search', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/c9search_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/mode/folding/c9search'], function(require, exports, module) {
var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var Tokenizer = require("../tokenizer").Tokenizer;
var C9SearchHighlightRules = require("./c9search_highlight_rules").C9SearchHighlightRules;
var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent;
var C9StyleFoldMode = require("./folding/c9search").FoldMode;
var Mode = function() {
this.$tokenizer = new Tokenizer(new C9SearchHighlightRules().getRules(), "i");
this.$outdent = new MatchingBraceOutdent();
this.foldingRules = new C9StyleFoldMode();
};
oop.inherits(Mode, TextMode);
(function() {
this.getNextLineIndent = function(state, line, tab) {
var indent = this.$getIndent(line);
return indent;
};
this.checkOutdent = function(state, line, input) {
return this.$outdent.checkOutdent(line, input);
};
this.autoOutdent = function(state, doc, row) {
this.$outdent.autoOutdent(doc, row);
};
}).call(Mode.prototype);
exports.Mode = Mode;
});
ace.define('ace/mode/c9search_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function(require, exports, module) {
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var C9SearchHighlightRules = function() {
// regexp must not have capturing parentheses. Use (?:) instead.
// regexps are ordered -> the first match is used
this.$rules = {
"start" : [
{
token : ["c9searchresults.constant.numeric", "c9searchresults.text", "c9searchresults.text"],
regex : "(^\\s+[0-9]+)(:\\s*)(.+)"
},
{
token : ["string", "text"], // single line
regex : "(.+)(:$)"
}
]
};
};
oop.inherits(C9SearchHighlightRules, TextHighlightRules);
exports.C9SearchHighlightRules = C9SearchHighlightRules;
});
ace.define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) {
var Range = require("../range").Range;
var MatchingBraceOutdent = function() {};
(function() {
this.checkOutdent = function(line, input) {
if (! /^\s+$/.test(line))
return false;
return /^\s*\}/.test(input);
};
this.autoOutdent = function(doc, row) {
var line = doc.getLine(row);
var match = line.match(/^(\s*\})/);
if (!match) return 0;
var column = match[1].length;
var openBracePos = doc.findMatchingBracket({row: row, column: column});
if (!openBracePos || openBracePos.row == row) return 0;
var indent = this.$getIndent(doc.getLine(openBracePos.row));
doc.replace(new Range(row, 0, row, column-1), indent);
};
this.$getIndent = function(line) {
var match = line.match(/^(\s+)/);
if (match) {
return match[1];
}
return "";
};
}).call(MatchingBraceOutdent.prototype);
exports.MatchingBraceOutdent = MatchingBraceOutdent;
});
ace.define('ace/mode/folding/c9search', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function(require, exports, module) {
var oop = require("../../lib/oop");
var Range = require("../../range").Range;
var BaseFoldMode = require("./fold_mode").FoldMode;
var FoldMode = exports.FoldMode = function() {};
oop.inherits(FoldMode, BaseFoldMode);
(function() {
this.foldingStartMarker = /^(\S.*\:|Searching for.*)$/;
this.foldingStopMarker = /^(\s+|Found.*)$/;
this.getFoldWidgetRange = function(session, foldStyle, row) {
var lines = session.doc.getAllLines(row);
var line = lines[row];
var level1 = /^(Found.*|Searching for.*)$/;
var level2 = /^(\S.*\:|\s*)$/;
var re = level1.test(line) ? level1 : level2;
if (this.foldingStartMarker.test(line)) {
for (var i = row + 1, l = session.getLength(); i < l; i++) {
if (re.test(lines[i]))
break;
}
return new Range(row, line.length, i, 0);
}
if (this.foldingStopMarker.test(line)) {
for (var i = row - 1; i >= 0; i--) {
line = lines[i];
if (re.test(line))
break;
}
return new Range(i, line.length, row, 0);
}
};
}).call(FoldMode.prototype);
});
ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) {
var Range = require("../../range").Range;
var FoldMode = exports.FoldMode = function() {};
(function() {
this.foldingStartMarker = null;
this.foldingStopMarker = null;
// must return "" if there's no fold, to enable caching
this.getFoldWidget = function(session, foldStyle, row) {
var line = session.getLine(row);
if (this.foldingStartMarker.test(line))
return "start";
if (foldStyle == "markbeginend"
&& this.foldingStopMarker
&& this.foldingStopMarker.test(line))
return "end";
return "";
};
this.getFoldWidgetRange = function(session, foldStyle, row) {
return null;
};
this.indentationBlock = function(session, row, column) {
var re = /\S/;
var line = session.getLine(row);
var startLevel = line.search(re);
if (startLevel == -1)
return;
var startColumn = column || line.length;
var maxRow = session.getLength();
var startRow = row;
var endRow = row;
while (++row < maxRow) {
var level = session.getLine(row).search(re);
if (level == -1)
continue;
if (level <= startLevel)
break;
endRow = row;
}
if (endRow > startRow) {
var endColumn = session.getLine(endRow).length;
return new Range(startRow, startColumn, endRow, endColumn);
}
};
this.openingBracketBlock = function(session, bracket, row, column, typeRe) {
var start = {row: row, column: column + 1};
var end = session.$findClosingBracket(bracket, start, typeRe);
if (!end)
return;
var fw = session.foldWidgets[end.row];
if (fw == null)
fw = this.getFoldWidget(session, end.row);
if (fw == "start" && end.row > start.row) {
end.row --;
end.column = session.getLine(end.row).length;
}
return Range.fromPoints(start, end);
};
}).call(FoldMode.prototype);
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
ace.define('ace/mode/latex', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/latex_highlight_rules', 'ace/range'], function(require, exports, module) {
var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var Tokenizer = require("../tokenizer").Tokenizer;
var LatexHighlightRules = require("./latex_highlight_rules").LatexHighlightRules;
var Range = require("../range").Range;
var Mode = function()
{
this.$tokenizer = new Tokenizer(new LatexHighlightRules().getRules());
};
oop.inherits(Mode, TextMode);
(function() {
this.toggleCommentLines = function(state, doc, startRow, endRow) {
// This code is adapted from ruby.js
var outdent = true;
// LaTeX comments begin with % and go to the end of the line
var commentRegEx = /^(\s*)\%/;
for (var i = startRow; i <= endRow; i++) {
if (!commentRegEx.test(doc.getLine(i))) {
outdent = false;
break;
}
}
if (outdent) {
var deleteRange = new Range(0, 0, 0, 0);
for (var i = startRow; i <= endRow; i++) {
var line = doc.getLine(i);
var m = line.match(commentRegEx);
deleteRange.start.row = i;
deleteRange.end.row = i;
deleteRange.end.column = m[0].length;
doc.replace(deleteRange, m[1]);
}
}
else {
doc.indentRows(startRow, endRow, "%");
}
};
// There is no universally accepted way of indenting a tex document
// so just maintain the indentation of the previous line
this.getNextLineIndent = function(state, line, tab) {
return this.$getIndent(line);
};
}).call(Mode.prototype);
exports.Mode = Mode;
});
ace.define('ace/mode/latex_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function(require, exports, module) {
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var LatexHighlightRules = function() {
this.$rules = {
"start" : [{
// A tex command e.g. \foo
token : "keyword",
regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)",
}, {
// Curly and square braces
token : "lparen",
regex : "[[({]"
}, {
// Curly and square braces
token : "rparen",
regex : "[\\])}]"
}, {
// Inline math between two $ symbols
token : "string",
regex : "\\$(?:(?:\\\\.)|(?:[^\\$\\\\]))*?\\$"
}, {
// A comment. Tex comments start with % and go to
// the end of the line
token : "comment",
regex : "%.*$"
}]
};
};
oop.inherits(LatexHighlightRules, TextHighlightRules);
exports.LatexHighlightRules = LatexHighlightRules;
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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