%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /data/old/home/stash/stash/atlassian-stash/static/feature/changeset/difftree/
Upload File :
Create Path :
Current File : //data/old/home/stash/stash/atlassian-stash/static/feature/changeset/difftree/difftree.js

define('feature/changeset/difftree', [
    'aui',
    'jquery',
    'stash/api/util/navbuilder',
    'util/ajax',
    'util/events',
    'model/content-tree-node-types',
    'model/path-and-line',
    'feature/changeset/difftree/difftree-search',
    'exports'
], function(
    AJS,
    $,
    navbuilder,
    ajax,
    events,
    ContentNodeType,
    PathAndLine,
    DiffTreeSearch,
    exports
) {

    /**
     * This is the data format expected for jstree.
     *
     * @typedef {object} DiffTreeData
     * @property {Array<DiffTreeData>} children
     * @property {Object} metadata
     * @property {{title: string, attr: Object}} data
     */

        var pathSeparator = '/';
        var DEFAULT_CHANGESET_LIMIT = 1000;
        var defaultMaximumOpen = 200;
        // Magic number based on our own PR usage - if there are < 50 files the diff is likely to be small (and can be cached)
        // See STASHDEV-7008 ticket for more details
        var MAX_FILES_SMALL_DIFF = 50;

        function openTree(tree, maximumOpen) {
            maximumOpen = maximumOpen >= 0 ? maximumOpen : defaultMaximumOpen;
            var opened = 0;
            function openNodes(node) {
                if (node.metadata.isDirectory) {
                    node.state = 'open';
                    node.data.icon = 'aui-icon aui-icon-small aui-iconfont-devtools-folder-open';

                    for (var i = 0, l = node.children.length, child; i < l && opened < maximumOpen; i++) {
                        child = node.children[i];
                        openNodes(child);
                    }
                } else if (node.metadata.isFile) {
                    // May have search result children
                    if (node.children && node.children.length > 0) {
                        node.state = 'open';
                    }
                    opened += (node.children ? node.children.length : 1);
                }
            }
            openNodes(tree);
            return tree;
        }

        function compareTreeNodes(a, b) {
            return a.children ?
                (b.children ? (a.data.title.toLowerCase() < b.data.title.toLowerCase() ? -1 : 1) : -1) :
                (!b.children ? (a.data.title.toLowerCase() < b.data.title.toLowerCase() ? -1 : 1) : 1);
        }

        function flattenTree(tree) {
            tree.childrenByTypeAndComponent = undefined;
            for (var i = 0, l = tree.children.length, child, components; i < l; i++) {
                child = tree.children[i];
                if (child.metadata.isDirectory) {
                    components = [child.data.title];
                    while (child.children.length === 1 && child.children[0].metadata.isDirectory) {
                        child = child.children[0];
                        components.push(child.data.title);
                    }
                    child.data.title = AJS.escapeHtml(components.join(pathSeparator));
                    tree.children[i] = child;
                    flattenTree(child);
                }
            }
            tree.children.sort(compareTreeNodes);
        }

        function computeTree(changes, maximumOpen) {


            var tree = {
                data : {
                    icon : "aui-icon aui-icon-small aui-iconfont-devtools-folder-closed"
                },
                state : 'closed',
                metadata : {
                    isDirectory : true
                },
                children : [],
                childrenByTypeAndComponent : {}
            };
            for (var i = 0, l = changes.length, change, subTree; i < l; i++) {
                change = changes[i];
                subTree = tree;
                for (var j = 0, k = change.path.components.length, component, key, child; j < k; j++) {
                    component = change.path.components[j];
                    key = (j + 1 === k ? 'F' : 'D') + component;
                    if (Object.prototype.hasOwnProperty.call(subTree.childrenByTypeAndComponent, key)) {
                        subTree = subTree.childrenByTypeAndComponent[key];
                    } else {
                        var isLastPathComponent = j + 1 === k;
                        if (isLastPathComponent) {
                            var hasComments = !!(change.attributes &&
                                                 change.attributes.activeComments &&
                                                 parseInt(change.attributes.activeComments[0], 10));
                            child = {
                                data : {
                                    title : AJS.escapeHtml(component),
                                    icon : "aui-icon aui-icon-small " + (change.nodeType === ContentNodeType.SUBMODULE ? 'aui-iconfont-devtools-submodule' : hasComments ? "aui-iconfont-devtools-file-commented" : "aui-iconfont-devtools-file"),
                                    attr : {
                                        id : 'change' + i,
                                        "class": "difftree-file change-type-" + change.type + (change.conflict ? " conflict" : ""),
                                        href : "#" + change.path.toString,
                                        title: change.conflict ? AJS.I18n.getText('stash.web.pullrequest.tree.conflicted.file') :
                                                            hasComments ? AJS.I18n.getText('stash.web.pullrequest.tree.commented.file') : undefined
                                    }
                                },
                                metadata : {
                                    isFile : true,
                                    changeType : change.type,
                                    nodeType : change.nodeType,
                                    path : change.path,
                                    srcPath : change.srcPath,
                                    conflict: change.conflict,
                                    contentId: change.contentId,
                                    executable: change.executable,
                                    srcExecutable: change.srcExecutable
                                }
                            };
                        } else {
                            child = {
                                data : {
                                    title : AJS.escapeHtml(component),
                                    icon : "aui-icon aui-icon-small aui-iconfont-devtools-folder-closed"
                                },
                                state : 'closed',
                                metadata : {
                                    isDirectory : true
                                },
                                children : [],
                                childrenByTypeAndComponent : {}
                            };
                        }
                        subTree.children.push(child);
                        subTree = subTree.childrenByTypeAndComponent[key] = child;
                    }
                }
            }
            flattenTree(tree);
            openTree(tree, maximumOpen);
            return tree;
        }

        /**
         * Get a builder to build the URL used to fetch the file changes.
         *
         * @param {number} start start index
         * @param {number} limit max number of changes
         * @param {CommitRange} commitRange commit range describing the source and target changesets
         * @returns {stash/api/util/navbuilder.Builder} a builder to build the URL used to fetch the file changes
         * @private
         */
        function urlBuilder(start, limit, commitRange) {
            return navbuilder.rest()
                .currentRepo()
                .changes(commitRange)
                .withParams({ start : start, limit : limit });
        }

        /**
         * Renders a tree of changed files.
         *
         * @param {string} wrapperSelector selector for the surrounding container
         * @param {string} toolbarSelector selector for toolbar (for adding search bar)
         * @param {CommitRange} commitRange commit range of the parent diff
         * @param {Object} [options] optional properties for the tree
         * @constructor
         */
        function DiffTree(wrapperSelector, toolbarSelector, commitRange, options) {
            options = options || {};

            this._fileLimit = options.maxChanges || DEFAULT_CHANGESET_LIMIT;
            this._$wrapper = $(wrapperSelector);
            this._$toolbar = $(toolbarSelector);
            this._commitRange = commitRange;
            this._hasOtherParents = !!options.hasOtherParents;
            this._urlBuilder = options.urlBuilder || urlBuilder;
            this._searchUrlBuilder = options.searchUrlBuilder;
        }

        _.extend(DiffTree.prototype, events.createEventMixin("diffTree", { localOnly : true }));

        DiffTree.prototype.init = function(selectedPathComponents) {

            this._destroyables = [];
            this._initiallySelectedPathComponents = selectedPathComponents;
            this._firstCommentAddedHandler = _.bind(this._firstCommentAddedHandler, this);
            this._lastCommentDeletedHandler =_.bind(this._lastCommentDeletedHandler, this);

            this._destroyables.push(events.chain()
                .on('stash.feature.comments.firstCommentAdded', this._firstCommentAddedHandler)
                .on('stash.feature.comments.lastCommentDeleted', this._lastCommentDeletedHandler)
            );

            if (this._searchUrlBuilder) {
                this._destroyables.push(this._addSearch());
            }

            if (!this.data) {
                return this.requestData();
            } else {
                return this.dataReceived(this.data);
            }
        };

        DiffTree.prototype._addSearch = function() {
            var $searchWrapper = $(stash.feature.difftree.searchWrapper());
            this._$toolbar.find('h4').replaceWith($searchWrapper);

            var search = new DiffTreeSearch.DiffTreeSearch();
            $searchWrapper.prepend(search.input.$el);

            var _destroyables = [];
            var self = this;
            _destroyables.push(search);
            _destroyables.push(events.chainWith($searchWrapper.find(".search-button-when-collapsed"))
                    .on('click', function() {
                        search.focusSearch();
                    })
            );
            _destroyables.push(events.chainWith(search).on('search-focus', _.bind(this.trigger, this, 'search-focus')));
            _destroyables.push({destroy: search.register(function(filter) {
                function isSmallDiff() {
                    return self._getFileCount(self.data, MAX_FILES_SMALL_DIFF) < MAX_FILES_SMALL_DIFF;
                }
                return ajax.rest({
                    // Use '' to search for everything
                    url: self._searchUrlBuilder({path: '', commitRange: self._commitRange.toJSON()})
                        .withParams({withComments: false})
                        // Pull Requests aren't cacheable (yet), so _always_ send the filter
                        // NOTE: We still do the filter client-side, but the cost is cheap
                        .withParams(filter && (self._commitRange.getPullRequest() || !isSmallDiff()) ? {filter: filter} : {})
                        .build()
                });
            }, function() {
                // Needs to be a lazy callback as 'data' may not be initialized yet
                return self.data;
            }).onValue(function(newData) {
                // Take a copy of the data before we re-render (so we can restore at a later point)
                self.data_copy = self.data;
                // Make sure we render the correct message
                self._searchEmpty = newData.data.children.length === 0;
                // TODO Yet another place where using events to talk directly to the diff-view :(
                events.trigger('internal.stash.feature.diffView.highlightSearch', null, newData.search);
                // Re-render the tree
                self.dataReceived(openTree(newData.data)).done(function() {
                    self.data = self.data_copy;
                    // Cleanup empty flag - no longer needed
                    self._searchEmpty = null;
                });
            })});
            return {
                destroy: function() {
                    _.invoke(_destroyables, 'destroy');
                    search.input.$el.remove();
                }
            };
        };

        DiffTree.prototype._getFileCount = function(data, max) {
            function countFiles(data) {
                return _.reduce(data.children, function(count, childData) {
                    if (count < max) {
                        return count + (childData.metadata.isFile ? 1 : countFiles(childData));
                    } else {
                        // Break out early - no point recursing any further
                        return count;
                    }
                }, 0);
            }
            return countFiles(data);
        };

        DiffTree.prototype._firstCommentAddedHandler = function() {
            var $icon = this.getSelectedFile().find('a > ins');
            $icon.hide()
                .removeClass('aui-iconfont-devtools-file').addClass('aui-iconfont-devtools-file-commented')
                .fadeIn('slow');
        };

        DiffTree.prototype._lastCommentDeletedHandler  = function() {
            var $icon = this.getSelectedFile().find('a > ins');
            $icon.hide()
                .removeClass('aui-iconfont-devtools-file-commented').addClass('aui-iconfont-devtools-file')
                .fadeIn('slow');
        };

        DiffTree.prototype.reset = function () {
            if (this._request) {
                this._request.abort();
                this._request = null;
                this._interrupted = true;
            }
            if (this._rendering) {
                this._rendering = false;
                this._interrupted = true;
            }
            _.invoke(this._destroyables, 'destroy');
        };

        DiffTree.prototype.requestData = function() {
            var self = this;

            if (this._request) {
                this._request.abort();
                this._request = null;
            }
            this._request = ajax.rest({
                url : self._urlBuilder(0, self._fileLimit, self._commitRange).build()
            });
            return this._request.always(function() {
                self._request = null;
            }).then(function(data) {
                if (!data) {
                    var msg = AJS.escapeHtml(AJS.I18n.getText('stash.web.pullrequest.tree.nodata'));
                    self.prependMessage(msg, "error");
                    return $.Deferred().reject();
                } else {
                    self._rendering = true;
                    self._interrupted = false;

                    self.isTruncated = !data.isLastPage;
                    return self.dataReceived(computeTree(data.values)).done(function() {
                        self._rendering = false;
                    });
                }
            });
        };

        function startsWith(str, substring) {
            return str.substring(0, substring.length) === substring;
        }

        /**
         * This function will return the file node whose path matches your preferredPathComponents if it can.
         * If it can't, it'll instead return the first file node in the tree.
         * If there are no file nodes, it'll return null;
         *
         * It's used for selecting an initial node in the file tree when the tree is being initialized.
         *
         * @param data a flattened tree (usually the diffTree.data object)
         * @param preferredPathComponents the path components of the file to attempt to select - array of strings.
         */
        function getNodeToSelect(data, preferredPathComponents) {
            return getPreferredNode(data, preferredPathComponents) || getFirstNode(data);
        }

        /**
         * @returns the file node which matches the preferredPathComponents, or null if none match
         */
        function getPreferredNode(preferred, preferredComponents) {
            if (!preferredComponents) {
                return null;
            }
            preferredComponents = preferredComponents.slice(0);

            while (preferred && preferred.children) {
                var componentToMatch = preferredComponents.shift(),
                    isLastComponent = !preferredComponents.length;

                var i = preferred.children.length;
                while (i--) {
                    var childToCheck = preferred.children[i],
                        title = childToCheck.data.title;

                    if (componentToMatch === title && isLastComponent === Boolean(childToCheck.metadata.isFile)) { //matches exactly, go inside.
                        preferred = childToCheck;
                        break;
                    }

                    // this is a collapsed node that at least partially matches, keep pulling off components
                    if (!isLastComponent && startsWith(title, componentToMatch + pathSeparator)) {

                        while (preferredComponents.length > 1 &&
                            startsWith(title, componentToMatch + pathSeparator + preferredComponents[0])) {
                            componentToMatch += pathSeparator;
                            componentToMatch += preferredComponents.shift();
                        }

                        if (title !== componentToMatch) { // they passed in a bad path, we're not going to find it.
                            //this handles:
                            // - preferredPath too short
                            // - preferredPath partially matches a collapsed node
                            return null;
                        }
                        //else collapsed node was fully matched.
                        preferred = childToCheck;
                        break;
                    }

                    //this child doesn't match
                }

                if (i < 0) { // no child matched
                    return null;
                }
            }

            return preferred && preferred.metadata && preferred.metadata.isFile ?
                preferred : null;
        }

        function getFirstNode(first) {
            while (first && first.children) {
                first = first.children[0];
            }
            return first && first.metadata && first.metadata.isFile ? first : null;
        }

        function getPathFromRoot(tree, toNode) {

            if (tree === toNode) {
                return [ toNode ];
            }

            var i = tree.children ? tree.children.length : 0;
            while(i--) {
                var childResult = getPathFromRoot(tree.children[i], toNode);
                if (childResult) {
                    childResult.unshift(tree);
                    return childResult;
                }
            }

            return null;
        }

        DiffTree.prototype.prependMessage = function (contents, type) {
            this._$wrapper.find(".aui-message").remove();

            type = type || "warning";

            this._$wrapper.find('.file-tree').before(aui.message[type]({
                extraClasses : 'diff-tree-scm-message',
                content : contents
            }));
        };

        DiffTree.prototype.dataReceived = function(data) {
            var self = this;
            this.data = data;

            var deferred = $.Deferred();
            function resolveIfNotInterrupted() {
                if (!self._interrupted) {
                    deferred.resolve(self);
                } else {
                    deferred.reject(self);
                }
            }

            var initiallySelectedNode = getNodeToSelect(this.data, this._initiallySelectedPathComponents);
            var initiallySelectedIdArray;

            if (initiallySelectedNode) {
                initiallySelectedIdArray = [ initiallySelectedNode.data.attr.id ];

                // open the ancestors of the selected node.
                var toOpen = getPathFromRoot(this.data, initiallySelectedNode) || [];

                toOpen.pop(); // don't open the file, just the folders above it. Otherwise the file gets a twixie.

                while(toOpen.length) {
                    toOpen.pop().state = 'open';
                }

            } else {
                initiallySelectedIdArray = [ ];
            }

            var initializingTree = true;

            var $currentlySelectedNode;

            this._$wrapper.find(".aui-message").remove();
            if (this.isTruncated) {
                var contents = "";

                if (this._commitRange.getPullRequest()){
                    //TODO - Better message for pull request changesets that are too large to render.
                    contents = AJS.escapeHtml(AJS.I18n.getText('stash.web.pullrequest.tree.truncated', this._fileLimit));
                } else {
                    var gitCommand,
                        atRevision = this._commitRange.getUntilRevision(),
                        parentRevision = this._commitRange.getSinceRevision();

                    if (parentRevision) {
                        gitCommand = 'git diff-tree -C -r ' + parentRevision.getId() + ' ' + atRevision.getId();
                    } else {
                        gitCommand = 'git diff-tree -r --root ' + atRevision.getId();
                    }

                    contents = AJS.escapeHtml(AJS.I18n.getText('stash.web.changeset.tree.truncated', this._fileLimit)) +
                        '<p class="scm-command">' + AJS.escapeHtml(gitCommand) + '</p>';
                }

                this.prependMessage(contents, "warning");
            }
            if (this.data.children.length) {
                this.$tree = this._$wrapper.children(".file-tree");
                this.$tree.fadeOut('fast', function () {
                        self.$tree.empty()
                            .off('.jstree')
                            .jstree("destroy")
                            .on('loaded.jstree', function() {
                                //allow jstree plugins to finish loading (namely ui).
                                setTimeout(function() {
                                    initializingTree = false;
                                    resolveIfNotInterrupted();
                                }, 0);
                            }).jstree({
                                json_data : {
                                    data : self.data.children,
                                    progressive_render : true
                                },
                                core : {
                                    html_titles: true,
                                    animation : 200
                                },
                                ui : {
                                    select_limit : 1,
                                    selected_parent_close: false,
                                    initially_select : initiallySelectedIdArray /* use this for deeplinking */
                                },
                                plugins : ["json_data", "ui"]
                            }).on('before.jstree', function(e, data) {
                                if (data.func === 'select_node') {
                                    var $node = $(data.args[0]).parent();

                                    if ($node.hasClass('jstree-leaf') && (!$currentlySelectedNode || $currentlySelectedNode[0] !== $node[0])) {
                                        $currentlySelectedNode = $node;
                                        events.trigger('stash.feature.changeset.difftree.selectedNodeChanged', self, $node, initializingTree);
                                    } else if ($node.length > 0) { // Node can be missing before tree has fully refreshed after a search clear
                                        self.$tree.jstree("toggle_node", $node);
                                        return false; //e.preventDefault() doesn't work...
                                    } // else { ignore everything else }
                                }
                            }).on('open_node.jstree', function(e, data){
                                var $openedNode = data.args[0];
                                // We may be opening a file with search results
                                if ($openedNode.data().isDirectory) {
                                    var $nodeIcon = $openedNode.children('a').children('ins');
                                    $nodeIcon.removeClass('aui-iconfont-devtools-folder-closed');
                                    $nodeIcon.addClass('aui-iconfont-devtools-folder-open');
                                }
                                events.trigger('stash.feature.changeset.difftree.nodeOpening', self, $openedNode);
                            }).on('after_open.jstree', function(e, data){
                                var $openedNode = data.args[0];
                                events.trigger('stash.feature.changeset.difftree.nodeOpened', self, $openedNode);
                            }).on('close_node.jstree', function(e, data){
                                var $closedNode = data.args[0];
                                if ($closedNode.data().isDirectory) {
                                    var $nodeIcon = $closedNode.children('a').children('ins');
                                    $nodeIcon.removeClass('aui-iconfont-devtools-folder-open');
                                    $nodeIcon.addClass('aui-iconfont-devtools-folder-closed');
                                }
                                events.trigger('stash.feature.changeset.difftree.nodeClosing', self, $closedNode);
                            }).on('after_close.jstree', function(e, data){
                                var $closedNode = data.args[0];
                                events.trigger('stash.feature.changeset.difftree.nodeClosed', self, $closedNode);
                            }).on('loaded.jstree', function(e, data){
                                events.trigger('stash.feature.changeset.difftree.treeInitialised', self, self);
                            }).fadeIn('fast');
                    });
            } else {
                this.$tree = undefined;
                var $fileTree = this._$wrapper.children(".file-tree");
                $fileTree.fadeOut('fast', function () {
                    $fileTree.empty().off('.jstree').jstree("destroy");
                    var message = AJS.escapeHtml(
                        self._searchEmpty ?
                            AJS.I18n.getText('stash.web.changeset.tree.emptysearch') :
                        self._hasOtherParents ?
                            AJS.I18n.getText('stash.web.changeset.merge.tree.empty') :
                            AJS.I18n.getText('stash.web.changeset.tree.empty')
                    );
                    self.prependMessage(message, "info");
                    setTimeout(resolveIfNotInterrupted, 0);
                });
            }
            return deferred.promise().always(function() {
                // This is purely for the browser tests - most reliable way of waiting for an update
                self._$wrapper.attr('data-last-updated', new Date().getTime());
            });
        };

        /**
         * @returns {?jQuery} the selected jstree node
         */
        DiffTree.prototype.getSelectedFile = function() {
            return this.$tree ? this.$tree.jstree('get_selected') : null;
        };

        /**
         * @returns {?jQuery} the selected jstree node or the jstree jquery element if nothing is selected.
         *                    Used by next/previous to select the first node.
         */
        DiffTree.prototype._getSelectedFileOrTree = function() {
            var $node = this.getSelectedFile();
            return $node && $node.length > 0 ? $node : this.$tree;
        };

        DiffTree.prototype.selectFile = function(pathComponents) {
            if (!this.$tree) {
                return;
            }
            var nodeToSelect = getNodeToSelect(this.data, pathComponents),
                currentlySelectedFile = this.getSelectedFile(),
                currentlySelectedPath = currentlySelectedFile &&  currentlySelectedFile.data('path'),
                currentlySelectedNode = currentlySelectedPath && getNodeToSelect(this.data, new PathAndLine(currentlySelectedPath).path.getComponents());

            if (nodeToSelect && nodeToSelect !== currentlySelectedNode) {
                // TODO STASHDEV-7022 - Fix back button for selecting lines (works for just files)
                // NOTE: This is only needed for state changes - everything is handled nicely
                this.$tree.jstree('deselect_all').jstree('select_node', '#' + nodeToSelect.data.attr.id);
            }
        };

        DiffTree.prototype.openNextFile = function() {
            if (this.$tree) {
                var jstree = $.jstree._reference(this.$tree),
                    $currentNode = this._getSelectedFileOrTree(),
                    $nextFile = findFile(jstree, jstree._get_next, jstree._get_next($currentNode));

                if ($nextFile && $nextFile.length) {
                    $nextFile.find('a').focus().click();
                }
            }
        };

        DiffTree.prototype.openPrevFile = function() {
            if (this.$tree) {
                var jstree = $.jstree._reference(this.$tree),
                    $currentNode = this._getSelectedFileOrTree(),
                    $prevFile = findPrevFileOrClosedDir(jstree, jstree._get_prev($currentNode));

                if ($prevFile && $prevFile.length) {
                    $prevFile.find('a').focus().click();
                }
            }
        };

        /* Find leaf based on the getAdjacentNode function passed in */
        function findFile(jstree, getAdjacentNode, $node) {
            if ($node && $node.length && !$node.hasClass('jstree-leaf')) {
                jstree.open_node($node);
                $node = findFile(jstree, getAdjacentNode, getAdjacentNode.call(jstree, $node));
            }

            return $node;
        }

        /* Traverse up til you find a leaf OR closed directory then find its last leaf */
        function findPrevFileOrClosedDir(jstree, $node) {
            if ($node && !$node.hasClass('jstree-leaf')) {
                if ($node.hasClass('jstree-closed')) {
                    jstree.open_node($node);
                    $node = findFile(jstree, getLastChild, getLastChild.call(jstree, $node));
                } else if ($node.length) {
                    $node = findPrevFileOrClosedDir(jstree, jstree._get_prev($node));
                }
            }

            return $node;
        }

        function getLastChild($node) {
            return this._get_children($node).filter('.jstree-last');
        }

        exports.DiffTree = DiffTree;
        exports.computeTree = computeTree;
        exports.flattenTree = flattenTree;
        exports._openTree = openTree;
        exports.compareTreeNodes = compareTreeNodes;
        exports.getNodeToSelect = getNodeToSelect;
        exports.getPathFromRoot = getPathFromRoot;
    });

Zerion Mini Shell 1.0