summaryrefslogtreecommitdiffstats
path: root/lib/redmine/menu_manager.rb
blob: db8e3e79cf8eb9dcfd651f57e4192854ba626109 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# frozen_string_literal: true

# Redmine - project management software
# Copyright (C) 2006-2020  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

module Redmine
  module MenuManager
    # @private
    class MenuError < StandardError
    end

    module MenuController
      def self.included(base)
        base.class_attribute :main_menu
        base.main_menu = true

        base.extend(ClassMethods)
      end

      module ClassMethods
        @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
        mattr_accessor :menu_items

        # Set the menu item name for a controller or specific actions
        # Examples:
        #   * menu_item :tickets # => sets the menu name to :tickets for the whole controller
        #   * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
        #   * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
        #
        # The default menu item name for a controller is controller_name by default
        # Eg. the default menu item name for ProjectsController is :projects
        def menu_item(id, options = {})
          if actions = options[:only]
            actions = [] << actions unless actions.is_a?(Array)
            actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
          else
            menu_items[controller_name.to_sym][:default] = id
          end
        end
      end

      def menu_items
        self.class.menu_items
      end

      def current_menu(project)
        if project && !project.new_record?
          :project_menu
        elsif self.class.main_menu
          :application_menu
        end
      end

      # Returns the menu item name according to the current action
      def current_menu_item
        @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
                                 menu_items[controller_name.to_sym][:default]
      end

      # Redirects user to the menu item
      # Returns false if user is not authorized
      def redirect_to_menu_item(name)
        redirect_to_project_menu_item(nil, name)
      end

      # Redirects user to the menu item of the given project
      # Returns false if user is not authorized
      def redirect_to_project_menu_item(project, name)
        menu = project.nil? ? :application_menu : :project_menu
        item = Redmine::MenuManager.items(menu).detect {|i| i.name.to_s == name.to_s}
        if item && item.allowed?(User.current, project)
          url = item.url
          url = {item.param => project}.merge(url) if project
          redirect_to url
          return true
        end
        false
      end
    end

    module MenuHelper
      # Returns the current menu item name
      def current_menu_item
        controller.current_menu_item
      end

      # Renders the application main menu
      def render_main_menu(project)
        if menu_name = controller.current_menu(project)
          render_menu(menu_name, project)
        end
      end

      def display_main_menu?(project)
        menu_name = controller.current_menu(project)
        menu_name.present? && Redmine::MenuManager.items(menu_name).children.present?
      end

      def render_menu(menu, project=nil)
        links = []
        menu_items_for(menu, project) do |node|
          links << render_menu_node(node, project)
        end
        links.empty? ? nil : content_tag('ul', links.join.html_safe)
      end

      def render_menu_node(node, project=nil)
        if node.children.present? || !node.child_menus.nil?
          return render_menu_node_with_children(node, project)
        else
          caption, url, selected = extract_node_details(node, project)
          return content_tag('li',
                             render_single_menu_node(node, caption, url, selected))
        end
      end

      def render_menu_node_with_children(node, project=nil)
        caption, url, selected = extract_node_details(node, project)

        html = [].tap do |html|
          html << '<li>'
          # Parent
          html << render_single_menu_node(node, caption, url, selected)

          # Standard children
          standard_children_list = "".html_safe.tap do |child_html|
            node.children.each do |child|
              child_html << render_menu_node(child, project) if allowed_node?(child, User.current, project)
            end
          end

          html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?

          # Unattached children
          unattached_children_list = render_unattached_children_menu(node, project)
          html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?

          html << '</li>'
        end
        return html.join("\n").html_safe
      end

      # Returns a list of unattached children menu items
      def render_unattached_children_menu(node, project)
        return nil unless node.child_menus

        "".html_safe.tap do |child_html|
          unattached_children = node.child_menus.call(project)
          # Tree nodes support #each so we need to do object detection
          if unattached_children.is_a? Array
            unattached_children.each do |child|
              child_html << content_tag(:li, render_unattached_menu_item(child, project)) if allowed_node?(child, User.current, project)
            end
          else
            raise MenuError, ":child_menus must be an array of MenuItems"
          end
        end
      end

      def render_single_menu_node(item, caption, url, selected)
        options = item.html_options(:selected => selected)

        # virtual nodes are only there for their children to be displayed in the menu
        # and should not do anything on click, except if otherwise defined elsewhere
        if url.blank?
          url = '#'
          options.reverse_merge!(:onclick => 'return false;')
        end
        link_to(h(caption), url, options)
      end

      def render_unattached_menu_item(menu_item, project)
        raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem

        if menu_item.allowed?(User.current, project)
          link_to(menu_item.caption, menu_item.url, menu_item.html_options)
        end
      end

      def menu_items_for(menu, project=nil)
        items = []
        Redmine::MenuManager.items(menu).root.children.each do |node|
          if node.allowed?(User.current, project)
            if block_given?
              yield node
            else
              items << node  # TODO: not used?
            end
          end
        end
        return block_given? ? nil : items
      end

      def extract_node_details(node, project=nil)
        item = node
        url =
          case item.url
          when Hash
            project.nil? ? item.url : {item.param => project}.merge(item.url)
          when Symbol
            if project
              send(item.url, project)
            else
              send(item.url)
            end
          else
            item.url
          end
        caption = item.caption(project)
        return [caption, url, (current_menu_item == item.name)]
      end

      # See MenuItem#allowed?
      def allowed_node?(node, user, project)
        raise MenuError, ":child_menus must be an array of MenuItems" unless node.is_a? MenuItem
        node.allowed?(user, project)
      end
    end

    class << self
      def map(menu_name)
        @items ||= {}
        mapper = Mapper.new(menu_name.to_sym, @items)
        if block_given?
          yield mapper
        else
          mapper
        end
      end

      def items(menu_name)
        @items[menu_name.to_sym] || MenuNode.new(:root, {})
      end
    end

    class Mapper
      attr_reader :menu, :menu_items

      def initialize(menu, items)
        items[menu] ||= MenuNode.new(:root, {})
        @menu = menu
        @menu_items = items[menu]
      end

      # Adds an item at the end of the menu. Available options:
      # * param: the parameter name that is used for the project id (default is :id)
      # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
      # * caption that can be:
      #   * a localized string Symbol
      #   * a String
      #   * a Proc that can take the project as argument
      # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
      # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
      # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
      #   eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
      # * last: menu item will stay at the end (eg. :last => true)
      # * html_options: a hash of html options that are passed to link_to
      def push(name, url, options={})
        options = options.dup

        if options[:parent]
          subtree = self.find(options[:parent])
          if subtree
            target_root = subtree
          else
            target_root = @menu_items.root
          end

        else
          target_root = @menu_items.root
        end

        # menu item position
        if first = options.delete(:first)
          target_root.prepend(MenuItem.new(name, url, options))
        elsif before = options.delete(:before)

          if exists?(before)
            target_root.add_at(MenuItem.new(name, url, options), position_of(before))
          else
            target_root.add(MenuItem.new(name, url, options))
          end

        elsif after = options.delete(:after)

          if exists?(after)
            target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
          else
            target_root.add(MenuItem.new(name, url, options))
          end

        elsif options[:last] # don't delete, needs to be stored
          target_root.add_last(MenuItem.new(name, url, options))
        else
          target_root.add(MenuItem.new(name, url, options))
        end
      end

      # Removes a menu item
      def delete(name)
        if found = self.find(name)
          @menu_items.remove!(found)
        end
      end

      # Checks if a menu item exists
      def exists?(name)
        @menu_items.any? {|node| node.name == name}
      end

      def find(name)
        @menu_items.find {|node| node.name == name}
      end

      def position_of(name)
        @menu_items.each do |node|
          if node.name == name
            return node.position
          end
        end
      end
    end

    class MenuNode
      include Enumerable
      attr_accessor :parent
      attr_reader :last_items_count, :name

      def initialize(name, content = nil)
        @name = name
        @children = []
        @last_items_count = 0
      end

      def children
        if block_given?
          @children.each {|child| yield child}
        else
          @children
        end
      end

      # Returns the number of descendants + 1
      def size
        @children.inject(1) {|sum, node| sum + node.size}
      end

      def each(&block)
        yield self
        children { |child| child.each(&block) }
      end

      # Adds a child at first position
      def prepend(child)
        add_at(child, 0)
      end

      # Adds a child at given position
      def add_at(child, position)
        raise "Child already added" if find {|node| node.name == child.name}

        @children = @children.insert(position, child)
        child.parent = self
        child
      end

      # Adds a child as last child
      def add_last(child)
        add_at(child, -1)
        @last_items_count += 1
        child
      end

      # Adds a child
      def add(child)
        position = @children.size - @last_items_count
        add_at(child, position)
      end
      alias :<< :add

      # Removes a child
      def remove!(child)
        @children.delete(child)
        @last_items_count -= +1 if child && child.last
        child.parent = nil
        child
      end

      # Returns the position for this node in it's parent
      def position
        self.parent.children.index(self)
      end

      # Returns the root for this node
      def root
        root = self
        root = root.parent while root.parent
        root
      end
    end

    class MenuItem < MenuNode
      include Redmine::I18n
      attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last, :permission

      def initialize(name, url, options={})
        raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
        raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
        raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
        raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
        @name = name
        @url = url
        @condition = options[:if]
        @permission = options[:permission]
        @permission ||= false if options.key?(:permission)
        @param = options[:param] || :id
        @caption = options[:caption]
        @html_options = options[:html] || {}
        # Adds a unique class to each menu item based on its name
        @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
        @parent = options[:parent]
        @child_menus = options[:children]
        @last = options[:last] || false
        super @name.to_sym
      end

      def caption(project=nil)
        if @caption.is_a?(Proc)
          c = @caption.call(project).to_s
          c = @name.to_s.humanize if c.blank?
          c
        else
          if @caption.nil?
            l_or_humanize(name, :prefix => 'label_')
          else
            @caption.is_a?(Symbol) ? l(@caption) : @caption
          end
        end
      end

      def html_options(options={})
        if options[:selected]
          o = @html_options.dup
          o[:class] += ' selected'
          o
        else
          @html_options
        end
      end

      # Checks if a user is allowed to access the menu item by:
      #
      # * Checking the permission or the url target (project only)
      # * Checking the conditions of the item
      def allowed?(user, project)
        if url.blank?
          # this is a virtual node that is only there for its children to be diplayed in the menu
          # it is considered an allowed node if at least one of the children is allowed
          all_children = children
          all_children += child_menus.call(project) if child_menus
          return false unless all_children.detect{|child| child.allowed?(user, project) }
        elsif user && project
          if permission
            unless user.allowed_to?(permission, project)
              return false
            end
          elsif permission.nil? && url.is_a?(Hash)
            unless user.allowed_to?(url, project)
              return false
            end
          end
        end
        if condition && !condition.call(project)
          # Condition that doesn't pass
          return false
        end
        return true
      end
    end
  end
end
ObjectDatabase getObjectDatabase(); /** * Create a new inserter to create objects in {@link #getObjectDatabase()}. * * @return a new inserter to create objects in {@link #getObjectDatabase()}. */ @NonNull public ObjectInserter newObjectInserter() { return getObjectDatabase().newInserter(); } /** * Create a new reader to read objects from {@link #getObjectDatabase()}. * * @return a new reader to read objects from {@link #getObjectDatabase()}. */ @NonNull public ObjectReader newObjectReader() { return getObjectDatabase().newReader(); } /** * Get the reference database which stores the reference namespace. * * @return the reference database which stores the reference namespace. */ @NonNull public abstract RefDatabase getRefDatabase(); /** * Get the configuration of this repository. * * @return the configuration of this repository. */ @NonNull public abstract StoredConfig getConfig(); /** * Create a new {@link org.eclipse.jgit.attributes.AttributesNodeProvider}. * * @return a new {@link org.eclipse.jgit.attributes.AttributesNodeProvider}. * This {@link org.eclipse.jgit.attributes.AttributesNodeProvider} * is lazy loaded only once. It means that it will not be updated * after loading. Prefer creating new instance for each use. * @since 4.2 */ @NonNull public abstract AttributesNodeProvider createAttributesNodeProvider(); /** * Get the used file system abstraction. * * @return the used file system abstraction, or {@code null} if * repository isn't local. */ /* * TODO This method should be annotated as Nullable, because in some * specific configurations metadata is not located in the local file system * (for example in memory databases). In "usual" repositories this * annotation would only cause compiler errors at places where the actual * directory can never be null. */ public FS getFS() { return fs; } /** * Open an object from this repository. * <p> * This is a one-shot call interface which may be faster than allocating a * {@link #newObjectReader()} to perform the lookup. * * @param objectId * identity of the object to open. * @return a {@link org.eclipse.jgit.lib.ObjectLoader} for accessing the * object. * @throws org.eclipse.jgit.errors.MissingObjectException * the object does not exist. * @throws java.io.IOException * the object store cannot be accessed. */ @NonNull public ObjectLoader open(AnyObjectId objectId) throws MissingObjectException, IOException { return getObjectDatabase().open(objectId); } /** * Open an object from this repository. * <p> * This is a one-shot call interface which may be faster than allocating a * {@link #newObjectReader()} to perform the lookup. * * @param objectId * identity of the object to open. * @param typeHint * hint about the type of object being requested, e.g. * {@link org.eclipse.jgit.lib.Constants#OBJ_BLOB}; * {@link org.eclipse.jgit.lib.ObjectReader#OBJ_ANY} if the * object type is not known, or does not matter to the caller. * @return a {@link org.eclipse.jgit.lib.ObjectLoader} for accessing the * object. * @throws org.eclipse.jgit.errors.MissingObjectException * the object does not exist. * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * typeHint was not OBJ_ANY, and the object's actual type does * not match typeHint. * @throws java.io.IOException * the object store cannot be accessed. */ @NonNull public ObjectLoader open(AnyObjectId objectId, int typeHint) throws MissingObjectException, IncorrectObjectTypeException, IOException { return getObjectDatabase().open(objectId, typeHint); } /** * Create a command to update, create or delete a ref in this repository. * * @param ref * name of the ref the caller wants to modify. * @return an update command. The caller must finish populating this command * and then invoke one of the update methods to actually make a * change. * @throws java.io.IOException * a symbolic ref was passed in and could not be resolved back * to the base ref, as the symbolic ref could not be read. */ @NonNull public RefUpdate updateRef(String ref) throws IOException { return updateRef(ref, false); } /** * Create a command to update, create or delete a ref in this repository. * * @param ref * name of the ref the caller wants to modify. * @param detach * true to create a detached head * @return an update command. The caller must finish populating this command * and then invoke one of the update methods to actually make a * change. * @throws java.io.IOException * a symbolic ref was passed in and could not be resolved back * to the base ref, as the symbolic ref could not be read. */ @NonNull public RefUpdate updateRef(String ref, boolean detach) throws IOException { return getRefDatabase().newUpdate(ref, detach); } /** * Create a command to rename a ref in this repository * * @param fromRef * name of ref to rename from * @param toRef * name of ref to rename to * @return an update command that knows how to rename a branch to another. * @throws java.io.IOException * the rename could not be performed. */ @NonNull public RefRename renameRef(String fromRef, String toRef) throws IOException { return getRefDatabase().newRename(fromRef, toRef); } /** * Parse a git revision string and return an object id. * * Combinations of these operators are supported: * <ul> * <li><b>HEAD</b>, <b>MERGE_HEAD</b>, <b>FETCH_HEAD</b></li> * <li><b>SHA-1</b>: a complete or abbreviated SHA-1</li> * <li><b>refs/...</b>: a complete reference name</li> * <li><b>short-name</b>: a short reference name under {@code refs/heads}, * {@code refs/tags}, or {@code refs/remotes} namespace</li> * <li><b>tag-NN-gABBREV</b>: output from describe, parsed by treating * {@code ABBREV} as an abbreviated SHA-1.</li> * <li><i>id</i><b>^</b>: first parent of commit <i>id</i>, this is the same * as {@code id^1}</li> * <li><i>id</i><b>^0</b>: ensure <i>id</i> is a commit</li> * <li><i>id</i><b>^n</b>: n-th parent of commit <i>id</i></li> * <li><i>id</i><b>~n</b>: n-th historical ancestor of <i>id</i>, by first * parent. {@code id~3} is equivalent to {@code id^1^1^1} or {@code id^^^}.</li> * <li><i>id</i><b>:path</b>: Lookup path under tree named by <i>id</i></li> * <li><i>id</i><b>^{commit}</b>: ensure <i>id</i> is a commit</li> * <li><i>id</i><b>^{tree}</b>: ensure <i>id</i> is a tree</li> * <li><i>id</i><b>^{tag}</b>: ensure <i>id</i> is a tag</li> * <li><i>id</i><b>^{blob}</b>: ensure <i>id</i> is a blob</li> * </ul> * * <p> * The following operators are specified by Git conventions, but are not * supported by this method: * <ul> * <li><b>ref@{n}</b>: n-th version of ref as given by its reflog</li> * <li><b>ref@{time}</b>: value of ref at the designated time</li> * </ul> * * @param revstr * A git object references expression * @return an ObjectId or {@code null} if revstr can't be resolved to any * ObjectId * @throws org.eclipse.jgit.errors.AmbiguousObjectException * {@code revstr} contains an abbreviated ObjectId and this * repository contains more than one object which match to the * input abbreviation. * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * the id parsed does not meet the type required to finish * applying the operators in the expression. * @throws org.eclipse.jgit.errors.RevisionSyntaxException * the expression is not supported by this implementation, or * does not meet the standard syntax. * @throws java.io.IOException * on serious errors */ @Nullable public ObjectId resolve(String revstr) throws AmbiguousObjectException, IncorrectObjectTypeException, RevisionSyntaxException, IOException { try (RevWalk rw = new RevWalk(this)) { rw.setRetainBody(false); Object resolved = resolve(rw, revstr); if (resolved instanceof String) { final Ref ref = findRef((String) resolved); return ref != null ? ref.getLeaf().getObjectId() : null; } return (ObjectId) resolved; } } /** * Simplify an expression, but unlike {@link #resolve(String)} it will not * resolve a branch passed or resulting from the expression, such as @{-}. * Thus this method can be used to process an expression to a method that * expects a branch or revision id. * * @param revstr * a {@link java.lang.String} object. * @return object id or ref name from resolved expression or {@code null} if * given expression cannot be resolved * @throws org.eclipse.jgit.errors.AmbiguousObjectException * if a shortened ObjectId was ambiguous * @throws java.io.IOException * if an IO error occurred */ @Nullable public String simplify(String revstr) throws AmbiguousObjectException, IOException { try (RevWalk rw = new RevWalk(this)) { rw.setRetainBody(true); Object resolved = resolve(rw, revstr); if (resolved != null) { if (resolved instanceof String) { return (String) resolved; } return ((AnyObjectId) resolved).getName(); } return null; } } @Nullable private Object resolve(RevWalk rw, String revstr) throws IOException { char[] revChars = revstr.toCharArray(); RevObject rev = null; String name = null; int done = 0; for (int i = 0; i < revChars.length; ++i) { switch (revChars[i]) { case '^': if (rev == null) { if (name == null) if (done == 0) name = new String(revChars, done, i); else { done = i + 1; break; } rev = parseSimple(rw, name); name = null; if (rev == null) return null; } if (i + 1 < revChars.length) { switch (revChars[i + 1]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': int j; rev = rw.parseCommit(rev); for (j = i + 1; j < revChars.length; ++j) { if (!Character.isDigit(revChars[j])) break; } String parentnum = new String(revChars, i + 1, j - i - 1); int pnum; try { pnum = Integer.parseInt(parentnum); } catch (NumberFormatException e) { RevisionSyntaxException rse = new RevisionSyntaxException( JGitText.get().invalidCommitParentNumber, revstr); rse.initCause(e); throw rse; } if (pnum != 0) { RevCommit commit = (RevCommit) rev; if (pnum > commit.getParentCount()) rev = null; else rev = commit.getParent(pnum - 1); } i = j - 1; done = j; break; case '{': int k; String item = null; for (k = i + 2; k < revChars.length; ++k) { if (revChars[k] == '}') { item = new String(revChars, i + 2, k - i - 2); break; } } i = k; if (item != null) if (item.equals("tree")) { //$NON-NLS-1$ rev = rw.parseTree(rev); } else if (item.equals("commit")) { //$NON-NLS-1$ rev = rw.parseCommit(rev); } else if (item.equals("blob")) { //$NON-NLS-1$ rev = rw.peel(rev); if (!(rev instanceof RevBlob)) throw new IncorrectObjectTypeException(rev, Constants.TYPE_BLOB); } else if (item.isEmpty()) { rev = rw.peel(rev); } else throw new RevisionSyntaxException(revstr); else throw new RevisionSyntaxException(revstr); done = k; break; default: rev = rw.peel(rev); if (rev instanceof RevCommit) { RevCommit commit = ((RevCommit) rev); if (commit.getParentCount() == 0) rev = null; else rev = commit.getParent(0); } else throw new IncorrectObjectTypeException(rev, Constants.TYPE_COMMIT); } } else { rev = rw.peel(rev); if (rev instanceof RevCommit) { RevCommit commit = ((RevCommit) rev); if (commit.getParentCount() == 0) rev = null; else rev = commit.getParent(0); } else throw new IncorrectObjectTypeException(rev, Constants.TYPE_COMMIT); } done = i + 1; break; case '~': if (rev == null) { if (name == null) if (done == 0) name = new String(revChars, done, i); else { done = i + 1; break; } rev = parseSimple(rw, name); name = null; if (rev == null) return null; } rev = rw.peel(rev); if (!(rev instanceof RevCommit)) throw new IncorrectObjectTypeException(rev, Constants.TYPE_COMMIT); int l; for (l = i + 1; l < revChars.length; ++l) { if (!Character.isDigit(revChars[l])) break; } int dist; if (l - i > 1) { String distnum = new String(revChars, i + 1, l - i - 1); try { dist = Integer.parseInt(distnum); } catch (NumberFormatException e) { RevisionSyntaxException rse = new RevisionSyntaxException( JGitText.get().invalidAncestryLength, revstr); rse.initCause(e); throw rse; } } else dist = 1; while (dist > 0) { RevCommit commit = (RevCommit) rev; if (commit.getParentCount() == 0) { rev = null; break; } commit = commit.getParent(0); rw.parseHeaders(commit); rev = commit; --dist; } i = l - 1; done = l; break; case '@': if (rev != null) throw new RevisionSyntaxException(revstr); if (i + 1 == revChars.length) continue; if (i + 1 < revChars.length && revChars[i + 1] != '{') continue; int m; String time = null; for (m = i + 2; m < revChars.length; ++m) { if (revChars[m] == '}') { time = new String(revChars, i + 2, m - i - 2); break; } } if (time != null) { if (time.equals("upstream")) { //$NON-NLS-1$ if (name == null) name = new String(revChars, done, i); if (name.isEmpty()) // Currently checked out branch, HEAD if // detached name = Constants.HEAD; if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$ throw new RevisionSyntaxException(MessageFormat .format(JGitText.get().invalidRefName, name), revstr); Ref ref = findRef(name); name = null; if (ref == null) return null; if (ref.isSymbolic()) ref = ref.getLeaf(); name = ref.getName(); RemoteConfig remoteConfig; try { remoteConfig = new RemoteConfig(getConfig(), "origin"); //$NON-NLS-1$ } catch (URISyntaxException e) { RevisionSyntaxException rse = new RevisionSyntaxException( revstr); rse.initCause(e); throw rse; } String remoteBranchName = getConfig() .getString( ConfigConstants.CONFIG_BRANCH_SECTION, Repository.shortenRefName(ref.getName()), ConfigConstants.CONFIG_KEY_MERGE); List<RefSpec> fetchRefSpecs = remoteConfig .getFetchRefSpecs(); for (RefSpec refSpec : fetchRefSpecs) { if (refSpec.matchSource(remoteBranchName)) { RefSpec expandFromSource = refSpec .expandFromSource(remoteBranchName); name = expandFromSource.getDestination(); break; } } if (name == null) throw new RevisionSyntaxException(revstr); } else if (time.matches("^-\\d+$")) { //$NON-NLS-1$ if (name != null) { throw new RevisionSyntaxException(revstr); } String previousCheckout = resolveReflogCheckout( -Integer.parseInt(time)); if (ObjectId.isId(previousCheckout)) { rev = parseSimple(rw, previousCheckout); } else { name = previousCheckout; } } else { if (name == null) name = new String(revChars, done, i); if (name.isEmpty()) name = Constants.HEAD; if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$ throw new RevisionSyntaxException(MessageFormat .format(JGitText.get().invalidRefName, name), revstr); Ref ref = findRef(name); name = null; if (ref == null) return null; // @{n} means current branch, not HEAD@{1} unless // detached if (ref.isSymbolic()) ref = ref.getLeaf(); rev = resolveReflog(rw, ref, time); } i = m; } else throw new RevisionSyntaxException(revstr); break; case ':': { RevTree tree; if (rev == null) { if (name == null) name = new String(revChars, done, i); if (name.isEmpty()) name = Constants.HEAD; rev = parseSimple(rw, name); name = null; } if (rev == null) return null; tree = rw.parseTree(rev); if (i == revChars.length - 1) return tree.copy(); TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), new String(revChars, i + 1, revChars.length - i - 1), tree); return tw != null ? tw.getObjectId(0) : null; } default: if (rev != null) throw new RevisionSyntaxException(revstr); } } if (rev != null) return rev.copy(); if (name != null) return name; if (done == revstr.length()) return null; name = revstr.substring(done); if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$ throw new RevisionSyntaxException( MessageFormat.format(JGitText.get().invalidRefName, name), revstr); if (findRef(name) != null) return name; return resolveSimple(name); } private static boolean isHex(char c) { return ('0' <= c && c <= '9') // || ('a' <= c && c <= 'f') // || ('A' <= c && c <= 'F'); } private static boolean isAllHex(String str, int ptr) { while (ptr < str.length()) { if (!isHex(str.charAt(ptr++))) return false; } return true; } @Nullable private RevObject parseSimple(RevWalk rw, String revstr) throws IOException { ObjectId id = resolveSimple(revstr); return id != null ? rw.parseAny(id) : null; } @Nullable private ObjectId resolveSimple(String revstr) throws IOException { if (ObjectId.isId(revstr)) return ObjectId.fromString(revstr); if (Repository.isValidRefName("x/" + revstr)) { //$NON-NLS-1$ Ref r = getRefDatabase().findRef(revstr); if (r != null) return r.getObjectId(); } if (AbbreviatedObjectId.isId(revstr)) return resolveAbbreviation(revstr); int dashg = revstr.indexOf("-g"); //$NON-NLS-1$ if ((dashg + 5) < revstr.length() && 0 <= dashg && isHex(revstr.charAt(dashg + 2)) && isHex(revstr.charAt(dashg + 3)) && isAllHex(revstr, dashg + 4)) { // Possibly output from git describe? String s = revstr.substring(dashg + 2); if (AbbreviatedObjectId.isId(s)) return resolveAbbreviation(s); } return null; } @Nullable private String resolveReflogCheckout(int checkoutNo) throws IOException { ReflogReader reader = getReflogReader(Constants.HEAD); if (reader == null) { return null; } List<ReflogEntry> reflogEntries = reader.getReverseEntries(); for (ReflogEntry entry : reflogEntries) { CheckoutEntry checkout = entry.parseCheckout(); if (checkout != null) if (checkoutNo-- == 1) return checkout.getFromBranch(); } return null; } private RevCommit resolveReflog(RevWalk rw, Ref ref, String time) throws IOException { int number; try { number = Integer.parseInt(time); } catch (NumberFormatException nfe) { RevisionSyntaxException rse = new RevisionSyntaxException( MessageFormat.format(JGitText.get().invalidReflogRevision, time)); rse.initCause(nfe); throw rse; } assert number >= 0; ReflogReader reader = getReflogReader(ref.getName()); if (reader == null) { throw new RevisionSyntaxException( MessageFormat.format(JGitText.get().reflogEntryNotFound, Integer.valueOf(number), ref.getName())); } ReflogEntry entry = reader.getReverseEntry(number); if (entry == null) throw new RevisionSyntaxException(MessageFormat.format( JGitText.get().reflogEntryNotFound, Integer.valueOf(number), ref.getName())); return rw.parseCommit(entry.getNewId()); } @Nullable private ObjectId resolveAbbreviation(String revstr) throws IOException, AmbiguousObjectException { AbbreviatedObjectId id = AbbreviatedObjectId.fromString(revstr); try (ObjectReader reader = newObjectReader()) { Collection<ObjectId> matches = reader.resolve(id); if (matches.isEmpty()) return null; else if (matches.size() == 1) return matches.iterator().next(); else throw new AmbiguousObjectException(id, matches); } } /** * Increment the use counter by one, requiring a matched {@link #close()}. */ public void incrementOpen() { useCnt.incrementAndGet(); } /** * {@inheritDoc} * <p> * Decrement the use count, and maybe close resources. */ @Override public void close() { int newCount = useCnt.decrementAndGet(); if (newCount == 0) { if (RepositoryCache.isCached(this)) { closedAt.set(System.currentTimeMillis()); } else { doClose(); } } else if (newCount == -1) { // should not happen, only log when useCnt became negative to // minimize number of log entries String message = MessageFormat.format(JGitText.get().corruptUseCnt, toString()); if (LOG.isDebugEnabled()) { LOG.debug(message, new IllegalStateException()); } else { LOG.warn(message); } if (RepositoryCache.isCached(this)) { closedAt.set(System.currentTimeMillis()); } } } /** * Invoked when the use count drops to zero during {@link #close()}. * <p> * The default implementation closes the object and ref databases. */ protected void doClose() { getObjectDatabase().close(); getRefDatabase().close(); } @Override @NonNull public String toString() { String desc; File directory = getDirectory(); if (directory != null) desc = directory.getPath(); else desc = getClass().getSimpleName() + "-" //$NON-NLS-1$ + System.identityHashCode(this); return "Repository[" + desc + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } /** * Get the name of the reference that {@code HEAD} points to. * <p> * This is essentially the same as doing: * * <pre> * return exactRef(Constants.HEAD).getTarget().getName() * </pre> * * Except when HEAD is detached, in which case this method returns the * current ObjectId in hexadecimal string format. * * @return name of current branch (for example {@code refs/heads/master}), * an ObjectId in hex format if the current branch is detached, or * {@code null} if the repository is corrupt and has no HEAD * reference. * @throws java.io.IOException * if an IO error occurred */ @Nullable public String getFullBranch() throws IOException { Ref head = exactRef(Constants.HEAD); if (head == null) { return null; } if (head.isSymbolic()) { return head.getTarget().getName(); } ObjectId objectId = head.getObjectId(); if (objectId != null) { return objectId.name(); } return null; } /** * Get the short name of the current branch that {@code HEAD} points to. * <p> * This is essentially the same as {@link #getFullBranch()}, except the * leading prefix {@code refs/heads/} is removed from the reference before * it is returned to the caller. * * @return name of current branch (for example {@code master}), an ObjectId * in hex format if the current branch is detached, or {@code null} * if the repository is corrupt and has no HEAD reference. * @throws java.io.IOException * if an IO error occurred */ @Nullable public String getBranch() throws IOException { String name = getFullBranch(); if (name != null) return shortenRefName(name); return null; } /** * Get the initial branch name of a new repository * * @return the initial branch name of a new repository * @since 5.11 */ protected @NonNull String getInitialBranch() { return initialBranch; } /** * Objects known to exist but not expressed by {@link #getAllRefs()}. * <p> * When a repository borrows objects from another repository, it can * advertise that it safely has that other repository's references, without * exposing any other details about the other repository. This may help a * client trying to push changes avoid pushing more than it needs to. * * @return unmodifiable collection of other known objects. * @throws IOException * if an IO error occurred */ @NonNull public Set<ObjectId> getAdditionalHaves() throws IOException { return Collections.emptySet(); } /** * Get a ref by name. * * @param name * the name of the ref to lookup. Must not be a short-hand form; * e.g., "master" is not automatically expanded to * "refs/heads/master". * @return the Ref with the given name, or {@code null} if it does not exist * @throws java.io.IOException * if an IO error occurred * @since 4.2 */ @Nullable public final Ref exactRef(String name) throws IOException { return getRefDatabase().exactRef(name); } /** * Search for a ref by (possibly abbreviated) name. * * @param name * the name of the ref to lookup. May be a short-hand form, e.g. * "master" which is automatically expanded to * "refs/heads/master" if "refs/heads/master" already exists. * @return the Ref with the given name, or {@code null} if it does not exist * @throws java.io.IOException * if an IO error occurred * @since 4.2 */ @Nullable public final Ref findRef(String name) throws IOException { return getRefDatabase().findRef(name); } /** * Get mutable map of all known refs, including symrefs like HEAD that may * not point to any object yet. * * @return mutable map of all known refs (heads, tags, remotes). * @deprecated use {@code getRefDatabase().getRefs()} instead. */ @Deprecated @NonNull public Map<String, Ref> getAllRefs() { try { return getRefDatabase().getRefs(RefDatabase.ALL); } catch (IOException e) { throw new UncheckedIOException(e); } } /** * Get mutable map of all tags * * @return mutable map of all tags; key is short tag name ("v1.0") and value * of the entry contains the ref with the full tag name * ("refs/tags/v1.0"). * @deprecated use {@code getRefDatabase().getRefsByPrefix(R_TAGS)} instead */ @Deprecated @NonNull public Map<String, Ref> getTags() { try { return getRefDatabase().getRefs(Constants.R_TAGS); } catch (IOException e) { throw new UncheckedIOException(e); } } /** * Peel a possibly unpeeled reference to an annotated tag. * <p> * If the ref cannot be peeled (as it does not refer to an annotated tag) * the peeled id stays null, but {@link org.eclipse.jgit.lib.Ref#isPeeled()} * will be true. * * @param ref * The ref to peel * @return <code>ref</code> if <code>ref.isPeeled()</code> is true; else a * new Ref object representing the same data as Ref, but isPeeled() * will be true and getPeeledObjectId will contain the peeled object * (or null). */ @NonNull private Ref peel(Ref ref) { try { return getRefDatabase().peel(ref); } catch (IOException e) { // Historical accident; if the reference cannot be peeled due // to some sort of repository access problem we claim that the // same as if the reference was not an annotated tag. return ref; } } /** * Get a map with all objects referenced by a peeled ref. * * @return a map with all objects referenced by a peeled ref. * @throws IOException * if an IO error occurred */ @NonNull public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() throws IOException { List<Ref> allRefs = getRefDatabase().getRefs(); Map<AnyObjectId, Set<Ref>> ret = new HashMap<>(allRefs.size()); for (Ref ref : allRefs) { ref = peel(ref); AnyObjectId target = ref.getPeeledObjectId(); if (target == null) target = ref.getObjectId(); // We assume most Sets here are singletons Set<Ref> oset = ret.put(target, Collections.singleton(ref)); if (oset != null) { // that was not the case (rare) if (oset.size() == 1) { // Was a read-only singleton, we must copy to a new Set oset = new HashSet<>(oset); } ret.put(target, oset); oset.add(ref); } } return ret; } /** * Get the index file location or {@code null} if repository isn't local. * * @return the index file location or {@code null} if repository isn't * local. * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @NonNull public File getIndexFile() throws NoWorkTreeException { if (isBare()) throw new NoWorkTreeException(); return indexFile; } /** * Locate a reference to a commit and immediately parse its content. * <p> * This method only returns successfully if the commit object exists, * is verified to be a commit, and was parsed without error. * * @param id * name of the commit object. * @return reference to the commit object. Never null. * @throws org.eclipse.jgit.errors.MissingObjectException * the supplied commit does not exist. * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * the supplied id is not a commit or an annotated tag. * @throws java.io.IOException * a pack file or loose object could not be read. * @since 4.8 */ public RevCommit parseCommit(AnyObjectId id) throws IncorrectObjectTypeException, IOException, MissingObjectException { if (id instanceof RevCommit && ((RevCommit) id).getRawBuffer() != null) { return (RevCommit) id; } try (RevWalk walk = new RevWalk(this)) { return walk.parseCommit(id); } } /** * Create a new in-core index representation and read an index from disk. * <p> * The new index will be read before it is returned to the caller. Read * failures are reported as exceptions and therefore prevent the method from * returning a partially populated index. * * @return a cache representing the contents of the specified index file (if * it exists) or an empty cache if the file does not exist. * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. * @throws java.io.IOException * the index file is present but could not be read. * @throws org.eclipse.jgit.errors.CorruptObjectException * the index file is using a format or extension that this * library does not support. */ @NonNull public DirCache readDirCache() throws NoWorkTreeException, CorruptObjectException, IOException { return DirCache.read(this); } /** * Create a new in-core index representation, lock it, and read from disk. * <p> * The new index will be locked and then read before it is returned to the * caller. Read failures are reported as exceptions and therefore prevent * the method from returning a partially populated index. * * @return a cache representing the contents of the specified index file (if * it exists) or an empty cache if the file does not exist. * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. * @throws java.io.IOException * the index file is present but could not be read, or the lock * could not be obtained. * @throws org.eclipse.jgit.errors.CorruptObjectException * the index file is using a format or extension that this * library does not support. */ @NonNull public DirCache lockDirCache() throws NoWorkTreeException, CorruptObjectException, IOException { // we want DirCache to inform us so that we can inform registered // listeners about index changes IndexChangedListener l = (IndexChangedEvent event) -> { notifyIndexChanged(true); }; return DirCache.lock(this, l); } /** * Get the repository state * * @return the repository state */ @NonNull public RepositoryState getRepositoryState() { if (isBare() || getDirectory() == null) return RepositoryState.BARE; // Pre Git-1.6 logic if (new File(getWorkTree(), ".dotest").exists()) //$NON-NLS-1$ return RepositoryState.REBASING; if (new File(getDirectory(), ".dotest-merge").exists()) //$NON-NLS-1$ return RepositoryState.REBASING_INTERACTIVE; // From 1.6 onwards if (new File(getDirectory(),"rebase-apply/rebasing").exists()) //$NON-NLS-1$ return RepositoryState.REBASING_REBASING; if (new File(getDirectory(),"rebase-apply/applying").exists()) //$NON-NLS-1$ return RepositoryState.APPLY; if (new File(getDirectory(),"rebase-apply").exists()) //$NON-NLS-1$ return RepositoryState.REBASING; if (new File(getDirectory(),"rebase-merge/interactive").exists()) //$NON-NLS-1$ return RepositoryState.REBASING_INTERACTIVE; if (new File(getDirectory(),"rebase-merge").exists()) //$NON-NLS-1$ return RepositoryState.REBASING_MERGE; // Both versions if (new File(getDirectory(), Constants.MERGE_HEAD).exists()) { // we are merging - now check whether we have unmerged paths try { if (!readDirCache().hasUnmergedPaths()) { // no unmerged paths -> return the MERGING_RESOLVED state return RepositoryState.MERGING_RESOLVED; } } catch (IOException e) { throw new UncheckedIOException(e); } return RepositoryState.MERGING; } if (new File(getDirectory(), "BISECT_LOG").exists()) //$NON-NLS-1$ return RepositoryState.BISECTING; if (new File(getDirectory(), Constants.CHERRY_PICK_HEAD).exists()) { try { if (!readDirCache().hasUnmergedPaths()) { // no unmerged paths return RepositoryState.CHERRY_PICKING_RESOLVED; } } catch (IOException e) { throw new UncheckedIOException(e); } return RepositoryState.CHERRY_PICKING; } if (new File(getDirectory(), Constants.REVERT_HEAD).exists()) { try { if (!readDirCache().hasUnmergedPaths()) { // no unmerged paths return RepositoryState.REVERTING_RESOLVED; } } catch (IOException e) { throw new UncheckedIOException(e); } return RepositoryState.REVERTING; } return RepositoryState.SAFE; } /** * Check validity of a ref name. It must not contain character that has * a special meaning in a Git object reference expression. Some other * dangerous characters are also excluded. * * For portability reasons '\' is excluded * * @param refName a {@link java.lang.String} object. * @return true if refName is a valid ref name */ public static boolean isValidRefName(String refName) { final int len = refName.length(); if (len == 0) { return false; } if (refName.endsWith(LOCK_SUFFIX)) { return false; } // Refs may be stored as loose files so invalid paths // on the local system must also be invalid refs. try { SystemReader.getInstance().checkPath(refName); } catch (CorruptObjectException e) { return false; } int components = 1; char p = '\0'; for (int i = 0; i < len; i++) { final char c = refName.charAt(i); if (c <= ' ') return false; switch (c) { case '.': switch (p) { case '\0': case '/': case '.': return false; } if (i == len -1) return false; break; case '/': if (i == 0 || i == len - 1) return false; if (p == '/') return false; components++; break; case '{': if (p == '@') return false; break; case '~': case '^': case ':': case '?': case '[': case '*': case '\\': case '\u007F': return false; } p = c; } return components > 1; } /** * Normalizes the passed branch name into a possible valid branch name. The * validity of the returned name should be checked by a subsequent call to * {@link #isValidRefName(String)}. * <p> * Future implementations of this method could be more restrictive or more * lenient about the validity of specific characters in the returned name. * <p> * The current implementation returns the trimmed input string if this is * already a valid branch name. Otherwise it returns a trimmed string with * special characters not allowed by {@link #isValidRefName(String)} * replaced by hyphens ('-') and blanks replaced by underscores ('_'). * Leading and trailing slashes, dots, hyphens, and underscores are removed. * * @param name * to normalize * @return The normalized name or an empty String if it is {@code null} or * empty. * @since 4.7 * @see #isValidRefName(String) */ public static String normalizeBranchName(String name) { if (name == null || name.isEmpty()) { return ""; //$NON-NLS-1$ } String result = name.trim(); String fullName = result.startsWith(Constants.R_HEADS) ? result : Constants.R_HEADS + result; if (isValidRefName(fullName)) { return result; } // All Unicode blanks to underscore result = result.replaceAll("(?:\\h|\\v)+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ StringBuilder b = new StringBuilder(); char p = '/'; for (int i = 0, len = result.length(); i < len; i++) { char c = result.charAt(i); if (c < ' ' || c == 127) { continue; } // Substitute a dash for problematic characters switch (c) { case '\\': case '^': case '~': case ':': case '?': case '*': case '[': case '@': case '<': case '>': case '|': case '"': c = '-'; break; default: break; } // Collapse multiple slashes, dashes, dots, underscores, and omit // dashes, dots, and underscores following a slash. switch (c) { case '/': if (p == '/') { continue; } p = '/'; break; case '.': case '_': case '-': if (p == '/' || p == '-') { continue; } p = '-'; break; default: p = c; break; } b.append(c); } // Strip trailing special characters, and avoid the .lock extension result = b.toString().replaceFirst("[/_.-]+$", "") //$NON-NLS-1$ //$NON-NLS-2$ .replaceAll("\\.lock($|/)", "_lock$1"); //$NON-NLS-1$ //$NON-NLS-2$ return FORBIDDEN_BRANCH_NAME_COMPONENTS.matcher(result) .replaceAll("$1+$2$3"); //$NON-NLS-1$ } /** * Strip work dir and return normalized repository path. * * @param workDir * Work dir * @param file * File whose path shall be stripped of its workdir * @return normalized repository relative path or the empty string if the * file is not relative to the work directory. */ @NonNull public static String stripWorkDir(File workDir, File file) { final String filePath = file.getPath(); final String workDirPath = workDir.getPath(); if (filePath.length() <= workDirPath.length() || filePath.charAt(workDirPath.length()) != File.separatorChar || !filePath.startsWith(workDirPath)) { File absWd = workDir.isAbsolute() ? workDir : workDir.getAbsoluteFile(); File absFile = file.isAbsolute() ? file : file.getAbsoluteFile(); if (absWd.equals(workDir) && absFile.equals(file)) { return ""; //$NON-NLS-1$ } return stripWorkDir(absWd, absFile); } String relName = filePath.substring(workDirPath.length() + 1); if (File.separatorChar != '/') { relName = relName.replace(File.separatorChar, '/'); } return relName; } /** * Whether this repository is bare * * @return true if this is bare, which implies it has no working directory. */ public boolean isBare() { return workTree == null; } /** * Get the root directory of the working tree, where files are checked out * for viewing and editing. * * @return the root directory of the working tree, where files are checked * out for viewing and editing. * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @NonNull public File getWorkTree() throws NoWorkTreeException { if (isBare()) throw new NoWorkTreeException(); return workTree; } /** * Force a scan for changed refs. Fires an IndexChangedEvent(false) if * changes are detected. * * @throws java.io.IOException * if an IO error occurred */ public abstract void scanForRepoChanges() throws IOException; /** * Notify that the index changed by firing an IndexChangedEvent. * * @param internal * {@code true} if the index was changed by the same * JGit process * @since 5.0 */ public abstract void notifyIndexChanged(boolean internal); /** * Get a shortened more user friendly ref name * * @param refName * a {@link java.lang.String} object. * @return a more user friendly ref name */ @NonNull public static String shortenRefName(String refName) { if (refName.startsWith(Constants.R_HEADS)) return refName.substring(Constants.R_HEADS.length()); if (refName.startsWith(Constants.R_TAGS)) return refName.substring(Constants.R_TAGS.length()); if (refName.startsWith(Constants.R_REMOTES)) return refName.substring(Constants.R_REMOTES.length()); return refName; } /** * Get a shortened more user friendly remote tracking branch name * * @param refName * a {@link java.lang.String} object. * @return the remote branch name part of <code>refName</code>, i.e. without * the <code>refs/remotes/&lt;remote&gt;</code> prefix, if * <code>refName</code> represents a remote tracking branch; * otherwise {@code null}. * @since 3.4 */ @Nullable public String shortenRemoteBranchName(String refName) { for (String remote : getRemoteNames()) { String remotePrefix = Constants.R_REMOTES + remote + "/"; //$NON-NLS-1$ if (refName.startsWith(remotePrefix)) return refName.substring(remotePrefix.length()); } return null; } /** * Get remote name * * @param refName * a {@link java.lang.String} object. * @return the remote name part of <code>refName</code>, i.e. without the * <code>refs/remotes/&lt;remote&gt;</code> prefix, if * <code>refName</code> represents a remote tracking branch; * otherwise {@code null}. * @since 3.4 */ @Nullable public String getRemoteName(String refName) { for (String remote : getRemoteNames()) { String remotePrefix = Constants.R_REMOTES + remote + "/"; //$NON-NLS-1$ if (refName.startsWith(remotePrefix)) return remote; } return null; } /** * Read the {@code GIT_DIR/description} file for gitweb. * * @return description text; null if no description has been configured. * @throws java.io.IOException * description cannot be accessed. * @since 4.6 */ @Nullable public String getGitwebDescription() throws IOException { return null; } /** * Set the {@code GIT_DIR/description} file for gitweb. * * @param description * new description; null to clear the description. * @throws java.io.IOException * description cannot be persisted. * @since 4.6 */ public void setGitwebDescription(@Nullable String description) throws IOException { throw new IOException(JGitText.get().unsupportedRepositoryDescription); } /** * Get the reflog reader * * @param refName * a {@link java.lang.String} object. * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied * refname, or {@code null} if the named ref does not exist. * @throws java.io.IOException * the ref could not be accessed. * @since 3.0 */ @Nullable public ReflogReader getReflogReader(String refName) throws IOException { return getRefDatabase().getReflogReader(refName); } /** * Get the reflog reader. Subclasses should override this method and provide * a more efficient implementation. * * @param ref * a Ref * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref. * @throws IOException * if an IO error occurred * @since 5.13.2 */ @NonNull public ReflogReader getReflogReader(@NonNull Ref ref) throws IOException { return getRefDatabase().getReflogReader(ref); } /** * Return the information stored in the file $GIT_DIR/MERGE_MSG. In this * file operations triggering a merge will store a template for the commit * message of the merge commit. * * @return a String containing the content of the MERGE_MSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public String readMergeCommitMsg() throws IOException, NoWorkTreeException { return readCommitMsgFile(Constants.MERGE_MSG); } /** * Write new content to the file $GIT_DIR/MERGE_MSG. In this file operations * triggering a merge will store a template for the commit message of the * merge commit. If <code>null</code> is specified as message the file will * be deleted. * * @param msg * the message which should be written or <code>null</code> to * delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeMergeCommitMsg(String msg) throws IOException { File mergeMsgFile = new File(gitDir, Constants.MERGE_MSG); writeCommitMsg(mergeMsgFile, msg); } /** * Return the information stored in the file $GIT_DIR/COMMIT_EDITMSG. In * this file hooks triggered by an operation may read or modify the current * commit message. * * @return a String containing the content of the COMMIT_EDITMSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. * @since 4.0 */ @Nullable public String readCommitEditMsg() throws IOException, NoWorkTreeException { return readCommitMsgFile(Constants.COMMIT_EDITMSG); } /** * Write new content to the file $GIT_DIR/COMMIT_EDITMSG. In this file hooks * triggered by an operation may read or modify the current commit message. * If {@code null} is specified as message the file will be deleted. * * @param msg * the message which should be written or {@code null} to delete * the file * @throws java.io.IOException * if an IO error occurred * @since 4.0 */ public void writeCommitEditMsg(String msg) throws IOException { File commiEditMsgFile = new File(gitDir, Constants.COMMIT_EDITMSG); writeCommitMsg(commiEditMsgFile, msg); } /** * Return the information stored in the file $GIT_DIR/MERGE_HEAD. In this * file operations triggering a merge will store the IDs of all heads which * should be merged together with HEAD. * * @return a list of commits which IDs are listed in the MERGE_HEAD file or * {@code null} if this file doesn't exist. Also if the file exists * but is empty {@code null} will be returned * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public List<ObjectId> readMergeHeads() throws IOException, NoWorkTreeException { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); byte[] raw = readGitDirectoryFile(Constants.MERGE_HEAD); if (raw == null) return null; List<ObjectId> heads = new ArrayList<>(); for (int p = 0; p < raw.length;) { heads.add(ObjectId.fromString(raw, p)); p = RawParseUtils .nextLF(raw, p + Constants.OBJECT_ID_STRING_LENGTH); } return heads; } /** * Write new merge-heads into $GIT_DIR/MERGE_HEAD. In this file operations * triggering a merge will store the IDs of all heads which should be merged * together with HEAD. If <code>null</code> is specified as list of commits * the file will be deleted * * @param heads * a list of commits which IDs should be written to * $GIT_DIR/MERGE_HEAD or <code>null</code> to delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeMergeHeads(List<? extends ObjectId> heads) throws IOException { writeHeadsFile(heads, Constants.MERGE_HEAD); } /** * Return the information stored in the file $GIT_DIR/CHERRY_PICK_HEAD. * * @return object id from CHERRY_PICK_HEAD file or {@code null} if this file * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public ObjectId readCherryPickHead() throws IOException, NoWorkTreeException { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); byte[] raw = readGitDirectoryFile(Constants.CHERRY_PICK_HEAD); if (raw == null) return null; return ObjectId.fromString(raw, 0); } /** * Return the information stored in the file $GIT_DIR/REVERT_HEAD. * * @return object id from REVERT_HEAD file or {@code null} if this file * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public ObjectId readRevertHead() throws IOException, NoWorkTreeException { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); byte[] raw = readGitDirectoryFile(Constants.REVERT_HEAD); if (raw == null) return null; return ObjectId.fromString(raw, 0); } /** * Write cherry pick commit into $GIT_DIR/CHERRY_PICK_HEAD. This is used in * case of conflicts to store the cherry which was tried to be picked. * * @param head * an object id of the cherry commit or <code>null</code> to * delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeCherryPickHead(ObjectId head) throws IOException { List<ObjectId> heads = (head != null) ? Collections.singletonList(head) : null; writeHeadsFile(heads, Constants.CHERRY_PICK_HEAD); } /** * Write revert commit into $GIT_DIR/REVERT_HEAD. This is used in case of * conflicts to store the revert which was tried to be picked. * * @param head * an object id of the revert commit or <code>null</code> to * delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeRevertHead(ObjectId head) throws IOException { List<ObjectId> heads = (head != null) ? Collections.singletonList(head) : null; writeHeadsFile(heads, Constants.REVERT_HEAD); } /** * Write original HEAD commit into $GIT_DIR/ORIG_HEAD. * * @param head * an object id of the original HEAD commit or <code>null</code> * to delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeOrigHead(ObjectId head) throws IOException { List<ObjectId> heads = head != null ? Collections.singletonList(head) : null; writeHeadsFile(heads, Constants.ORIG_HEAD); } /** * Return the information stored in the file $GIT_DIR/ORIG_HEAD. * * @return object id from ORIG_HEAD file or {@code null} if this file * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public ObjectId readOrigHead() throws IOException, NoWorkTreeException { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); byte[] raw = readGitDirectoryFile(Constants.ORIG_HEAD); return raw != null ? ObjectId.fromString(raw, 0) : null; } /** * Return the information stored in the file $GIT_DIR/SQUASH_MSG. In this * file operations triggering a squashed merge will store a template for the * commit message of the squash commit. * * @return a String containing the content of the SQUASH_MSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException * if an IO error occurred * @throws NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. */ @Nullable public String readSquashCommitMsg() throws IOException { return readCommitMsgFile(Constants.SQUASH_MSG); } /** * Write new content to the file $GIT_DIR/SQUASH_MSG. In this file * operations triggering a squashed merge will store a template for the * commit message of the squash commit. If <code>null</code> is specified as * message the file will be deleted. * * @param msg * the message which should be written or <code>null</code> to * delete the file * @throws java.io.IOException * if an IO error occurred */ public void writeSquashCommitMsg(String msg) throws IOException { File squashMsgFile = new File(gitDir, Constants.SQUASH_MSG); writeCommitMsg(squashMsgFile, msg); } @Nullable private String readCommitMsgFile(String msgFilename) throws IOException { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); File mergeMsgFile = new File(getDirectory(), msgFilename); try { return RawParseUtils.decode(IO.readFully(mergeMsgFile)); } catch (FileNotFoundException e) { if (mergeMsgFile.exists()) { throw e; } // the file has disappeared in the meantime ignore it return null; } } private void writeCommitMsg(File msgFile, String msg) throws IOException { if (msg != null) { try (FileOutputStream fos = new FileOutputStream(msgFile)) { fos.write(msg.getBytes(UTF_8)); } } else { FileUtils.delete(msgFile, FileUtils.SKIP_MISSING); } } /** * Read a file from the git directory. * * @param filename * the file to read * @return the raw contents or {@code null} if the file doesn't exist or is * empty * @throws IOException * if an IO error occurred */ private byte[] readGitDirectoryFile(String filename) throws IOException { File file = new File(getDirectory(), filename); try { byte[] raw = IO.readFully(file); return raw.length > 0 ? raw : null; } catch (FileNotFoundException notFound) { if (file.exists()) { throw notFound; } return null; } } /** * Write the given heads to a file in the git directory. * * @param heads * a list of object ids to write or null if the file should be * deleted. * @param filename * name of the file to write heads to * @throws FileNotFoundException * if the heads file couldn't be found * @throws IOException * if an IO error occurred */ private void writeHeadsFile(List<? extends ObjectId> heads, String filename) throws FileNotFoundException, IOException { File headsFile = new File(getDirectory(), filename); if (heads != null) { try (OutputStream bos = new BufferedOutputStream( new FileOutputStream(headsFile))) { for (ObjectId id : heads) { id.copyTo(bos); bos.write('\n'); } } } else { FileUtils.delete(headsFile, FileUtils.SKIP_MISSING); } } /** * Read a file formatted like the git-rebase-todo file. The "done" file is * also formatted like the git-rebase-todo file. These files can be found in * .git/rebase-merge/ or .git/rebase-append/ folders. * * @param path * path to the file relative to the repository's git-dir. E.g. * "rebase-merge/git-rebase-todo" or "rebase-append/done" * @param includeComments * <code>true</code> if also comments should be reported * @return the list of steps * @throws java.io.IOException * if an IO error occurred * @since 3.2 */ @NonNull public List<RebaseTodoLine> readRebaseTodo(String path, boolean includeComments) throws IOException { return new RebaseTodoFile(this).readRebaseTodo(path, includeComments); } /** * Write a file formatted like a git-rebase-todo file. * * @param path * path to the file relative to the repository's git-dir. E.g. * "rebase-merge/git-rebase-todo" or "rebase-append/done" * @param steps * the steps to be written * @param append * whether to append to an existing file or to write a new file * @throws java.io.IOException * if an IO error occurred * @since 3.2 */ public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps, boolean append) throws IOException { new RebaseTodoFile(this).writeRebaseTodoFile(path, steps, append); } /** * Get the names of all known remotes * * @return the names of all known remotes * @since 3.4 */ @NonNull public Set<String> getRemoteNames() { return getConfig() .getSubsections(ConfigConstants.CONFIG_REMOTE_SECTION); } /** * Check whether any housekeeping is required; if yes, run garbage * collection; if not, exit without performing any work. Some JGit commands * run autoGC after performing operations that could create many loose * objects. * <p> * Currently this option is supported for repositories of type * {@code FileRepository} only. See * {@link org.eclipse.jgit.internal.storage.file.GC#setAuto(boolean)} for * configuration details. * * @param monitor * to report progress * @since 4.6 */ public void autoGC(ProgressMonitor monitor) { // default does nothing } }