1from __future__ import division, absolute_import, unicode_literals
2
3from qtpy import QtWidgets
4from qtpy import QtCore
5from qtpy.QtCore import Qt
6from qtpy.QtCore import Signal
7
8from ..i18n import N_
9from ..interaction import Interaction
10from ..qtutils import get
11from .. import gitcmds
12from .. import icons
13from .. import qtutils
14from . import defs
15from . import completion
16from . import standard
17
18
19def create_new_branch(context, revision=''):
20    """Launches a dialog for creating a new branch"""
21    view = CreateBranchDialog(context, parent=qtutils.active_window())
22    if revision:
23        view.set_revision(revision)
24    view.show()
25    return view
26
27
28class CreateOpts(object):
29    def __init__(self, context):
30        self.context = context
31        self.reset = False
32        self.track = False
33        self.fetch = True
34        self.checkout = True
35        self.revision = 'HEAD'
36        self.branch = ''
37
38
39class CreateThread(QtCore.QThread):
40    command = Signal(object, object, object)
41    result = Signal(object)
42
43    def __init__(self, opts, parent):
44        QtCore.QThread.__init__(self, parent)
45        self.opts = opts
46
47    def run(self):
48        branch = self.opts.branch
49        revision = self.opts.revision
50        reset = self.opts.reset
51        checkout = self.opts.checkout
52        track = self.opts.track
53        context = self.opts.context
54        git = context.git
55        model = context.model
56        results = []
57        status = 0
58
59        if track and '/' in revision:
60            remote = revision.split('/', 1)[0]
61            status, out, err = git.fetch(remote)
62            self.command.emit(status, out, err)
63            results.append(('fetch', status, out, err))
64
65        if status == 0:
66            status, out, err = model.create_branch(
67                branch, revision, force=reset, track=track
68            )
69            self.command.emit(status, out, err)
70
71        results.append(('branch', status, out, err))
72        if status == 0 and checkout:
73            status, out, err = git.checkout(branch)
74            self.command.emit(status, out, err)
75            results.append(('checkout', status, out, err))
76
77        model.update_status()
78        self.result.emit(results)
79
80
81class CreateBranchDialog(standard.Dialog):
82    """A dialog for creating branches."""
83
84    def __init__(self, context, parent=None):
85        standard.Dialog.__init__(self, parent=parent)
86        self.setWindowTitle(N_('Create Branch'))
87        if parent is not None:
88            self.setWindowModality(Qt.WindowModal)
89
90        self.context = context
91        self.model = context.model
92        self.opts = CreateOpts(context)
93        self.thread = CreateThread(self.opts, self)
94
95        self.progress = None
96
97        self.branch_name_label = QtWidgets.QLabel()
98        self.branch_name_label.setText(N_('Branch Name'))
99
100        self.branch_name = completion.GitCreateBranchLineEdit(context)
101        self.branch_validator = completion.BranchValidator(
102            context.git, parent=self.branch_name
103        )
104        self.branch_name.setValidator(self.branch_validator)
105
106        self.rev_label = QtWidgets.QLabel()
107        self.rev_label.setText(N_('Starting Revision'))
108
109        self.revision = completion.GitRefLineEdit(context)
110        current = gitcmds.current_branch(context)
111        if current:
112            self.revision.setText(current)
113
114        self.local_radio = qtutils.radio(text=N_('Local branch'), checked=True)
115        self.remote_radio = qtutils.radio(text=N_('Tracking branch'))
116        self.tag_radio = qtutils.radio(text=N_('Tag'))
117
118        self.branch_list = QtWidgets.QListWidget()
119
120        self.update_existing_label = QtWidgets.QLabel()
121        self.update_existing_label.setText(N_('Update Existing Branch:'))
122
123        self.no_update_radio = qtutils.radio(text=N_('No'))
124        self.ffwd_only_radio = qtutils.radio(text=N_('Fast Forward Only'), checked=True)
125        self.reset_radio = qtutils.radio(text=N_('Reset'))
126
127        text = N_('Fetch Tracking Branch')
128        self.fetch_checkbox = qtutils.checkbox(text=text, checked=True)
129
130        text = N_('Checkout After Creation')
131        self.checkout_checkbox = qtutils.checkbox(text=text, checked=True)
132
133        icon = icons.branch()
134        self.create_button = qtutils.create_button(
135            text=N_('Create Branch'), icon=icon, default=True
136        )
137        self.close_button = qtutils.close_button()
138
139        self.options_checkbox_layout = qtutils.hbox(
140            defs.margin,
141            defs.spacing,
142            self.fetch_checkbox,
143            self.checkout_checkbox,
144            qtutils.STRETCH,
145        )
146
147        self.branch_name_layout = qtutils.hbox(
148            defs.margin, defs.spacing, self.branch_name_label, self.branch_name
149        )
150
151        self.rev_radio_group = qtutils.buttongroup(
152            self.local_radio, self.remote_radio, self.tag_radio
153        )
154
155        self.rev_radio_layout = qtutils.hbox(
156            defs.margin,
157            defs.spacing,
158            self.local_radio,
159            self.remote_radio,
160            self.tag_radio,
161            qtutils.STRETCH,
162        )
163
164        self.rev_start_textinput_layout = qtutils.hbox(
165            defs.no_margin, defs.spacing, self.rev_label, defs.spacing, self.revision
166        )
167
168        self.rev_start_layout = qtutils.vbox(
169            defs.no_margin,
170            defs.spacing,
171            self.rev_radio_layout,
172            self.branch_list,
173            self.rev_start_textinput_layout,
174        )
175
176        self.options_radio_group = qtutils.buttongroup(
177            self.no_update_radio, self.ffwd_only_radio, self.reset_radio
178        )
179
180        self.options_radio_layout = qtutils.hbox(
181            defs.no_margin,
182            defs.spacing,
183            self.update_existing_label,
184            self.no_update_radio,
185            self.ffwd_only_radio,
186            self.reset_radio,
187            qtutils.STRETCH,
188        )
189
190        self.buttons_layout = qtutils.hbox(
191            defs.margin,
192            defs.spacing,
193            self.close_button,
194            qtutils.STRETCH,
195            self.create_button,
196        )
197
198        self.main_layout = qtutils.vbox(
199            defs.margin,
200            defs.spacing,
201            self.branch_name_layout,
202            self.rev_start_layout,
203            defs.button_spacing,
204            self.options_radio_layout,
205            self.options_checkbox_layout,
206            self.buttons_layout,
207        )
208        self.setLayout(self.main_layout)
209
210        qtutils.add_close_action(self)
211        qtutils.connect_button(self.close_button, self.close)
212        qtutils.connect_button(self.create_button, self.create_branch)
213        qtutils.connect_toggle(self.local_radio, self.display_model)
214        qtutils.connect_toggle(self.remote_radio, self.display_model)
215        qtutils.connect_toggle(self.tag_radio, self.display_model)
216
217        branches = self.branch_list
218        # pylint: disable=no-member
219        branches.itemSelectionChanged.connect(self.branch_item_changed)
220
221        thread = self.thread
222        thread.command.connect(Interaction.log_status, type=Qt.QueuedConnection)
223        thread.result.connect(self.thread_result, type=Qt.QueuedConnection)
224
225        self.init_size(settings=context.settings, parent=parent)
226        self.display_model()
227
228    def set_revision(self, revision):
229        self.revision.setText(revision)
230
231    def getopts(self):
232        self.opts.revision = get(self.revision)
233        self.opts.branch = get(self.branch_name)
234        self.opts.checkout = get(self.checkout_checkbox)
235        self.opts.reset = get(self.reset_radio)
236        self.opts.fetch = get(self.fetch_checkbox)
237        self.opts.track = get(self.remote_radio)
238
239    def create_branch(self):
240        """Creates a branch; called by the "Create Branch" button"""
241        context = self.context
242        self.getopts()
243        revision = self.opts.revision
244        branch = self.opts.branch
245        no_update = get(self.no_update_radio)
246        ffwd_only = get(self.ffwd_only_radio)
247        existing_branches = gitcmds.branch_list(context)
248        check_branch = False
249
250        if not branch or not revision:
251            Interaction.critical(
252                N_('Missing Data'),
253                N_('Please provide both a branch ' 'name and revision expression.'),
254            )
255            return
256        if branch in existing_branches:
257            if no_update:
258                msg = N_('Branch "%s" already exists.') % branch
259                Interaction.critical(N_('Branch Exists'), msg)
260                return
261            # Whether we should prompt the user for lost commits
262            commits = gitcmds.rev_list_range(context, revision, branch)
263            check_branch = bool(commits)
264
265        if check_branch:
266            msg = N_(
267                'Resetting "%(branch)s" to "%(revision)s" ' 'will lose commits.'
268            ) % dict(branch=branch, revision=revision)
269            if ffwd_only:
270                Interaction.critical(N_('Branch Exists'), msg)
271                return
272            lines = [msg]
273            for idx, commit in enumerate(commits):
274                subject = commit[1][0 : min(len(commit[1]), 16)]
275                if len(subject) < len(commit[1]):
276                    subject += '...'
277                lines.append('\t' + commit[0][:8] + '\t' + subject)
278                if idx >= 5:
279                    skip = len(commits) - 5
280                    lines.append('\t(%s)' % (N_('%d skipped') % skip))
281                    break
282            line = N_('Recovering lost commits may not be easy.')
283            lines.append(line)
284
285            info_text = N_('Reset "%(branch)s" to "%(revision)s"?') % dict(
286                branch=branch, revision=revision
287            )
288
289            if not Interaction.confirm(
290                N_('Reset Branch?'),
291                '\n'.join(lines),
292                info_text,
293                N_('Reset Branch'),
294                default=False,
295                icon=icons.undo(),
296            ):
297                return
298
299        title = N_('Create Branch')
300        label = N_('Updating')
301        self.progress = standard.progress(title, label, self)
302        self.progress.show()
303        self.thread.start()
304
305    def thread_result(self, results):
306        self.progress.hide()
307        del self.progress
308
309        for (cmd, status, _, _) in results:
310            if status != 0:
311                Interaction.critical(
312                    N_('Error Creating Branch'),
313                    (
314                        N_('"%(command)s" returned exit status "%(status)d"')
315                        % dict(command='git ' + cmd, status=status)
316                    ),
317                )
318                return
319
320        self.accept()
321
322    # pylint: disable=unused-argument
323    def branch_item_changed(self, *rest):
324        """This callback is called when the branch selection changes"""
325        # When the branch selection changes then we should update
326        # the "Revision Expression" accordingly.
327        qlist = self.branch_list
328        remote_branch = qtutils.selected_item(qlist, self.branch_sources())
329        if not remote_branch:
330            return
331        # Update the model with the selection
332        self.revision.setText(remote_branch)
333
334        # Set the branch field if we're branching from a remote branch.
335        if not get(self.remote_radio):
336            return
337        branch = gitcmds.strip_remote(self.model.remotes, remote_branch)
338        if branch == 'HEAD':
339            return
340        # Signal that we've clicked on a remote branch
341        self.branch_name.set_value(branch)
342
343    def display_model(self):
344        """Sets the branch list to the available branches"""
345        branches = self.branch_sources()
346        qtutils.set_items(self.branch_list, branches)
347
348    def branch_sources(self):
349        """Get the list of items for populating the branch root list."""
350        if get(self.local_radio):
351            value = self.model.local_branches
352        elif get(self.remote_radio):
353            value = self.model.remote_branches
354        elif get(self.tag_radio):
355            value = self.model.tags
356        else:
357            value = []
358        return value
359
360    def dispose(self):
361        self.branch_name.dispose()
362        self.revision.dispose()
363