1from __future__ import division, absolute_import, unicode_literals
2
3from .. import cmds
4from .. import core
5from .. import observable
6from .. import gitcmds
7from .. import utils
8from ..i18n import N_
9from ..git import STDOUT
10from ..interaction import Interaction
11
12
13class StashModel(observable.Observable):
14    def __init__(self, context):
15        observable.Observable.__init__(self)
16        self.context = context
17        self.git = context.git
18        self.model = model = context.model
19        if not model.initialized:
20            model.update_status()
21
22    def stash_list(self, *args):
23        return self.git.stash('list', *args)[STDOUT].splitlines()
24
25    def is_staged(self):
26        return bool(self.model.staged)
27
28    def is_changed(self):
29        model = self.model
30        return bool(model.modified or model.staged)
31
32    def stash_info(self, revids=False, names=False):
33        """Parses "git stash list" and returns a list of stashes."""
34        stashes = self.stash_list(r'--format=%gd/%aD/%s')
35        split_stashes = [s.split('/', 3) for s in stashes if s]
36        stashes = ['{0}: {1}'.format(s[0], s[2]) for s in split_stashes]
37        revids = [s[0] for s in split_stashes]
38        author_dates = [s[1] for s in split_stashes]
39        names = [s[2] for s in split_stashes]
40
41        return stashes, revids, author_dates, names
42
43    def stash_diff(self, rev):
44        git = self.git
45        diffstat = git.stash('show', rev)[STDOUT]
46        diff = git.stash('show', '-p', '--no-ext-diff', rev)[STDOUT]
47        return diffstat + '\n\n' + diff
48
49
50class ApplyStash(cmds.ContextCommand):
51    def __init__(self, context, stash_index, index, pop):
52        super(ApplyStash, self).__init__(context)
53        self.stash_ref = 'refs/' + stash_index
54        self.index = index
55        self.pop = pop
56
57    def do(self):
58        ref = self.stash_ref
59        pop = self.pop
60        if pop:
61            action = 'pop'
62        else:
63            action = 'apply'
64        if self.index:
65            args = [action, '--index', ref]
66        else:
67            args = [action, ref]
68        status, out, err = self.git.stash(*args)
69        if status == 0:
70            Interaction.log_status(status, out, err)
71        else:
72            title = N_('Error')
73            cmdargs = core.list2cmdline(args)
74            Interaction.command_error(title, 'git stash ' + cmdargs, status, out, err)
75        self.model.update_status()
76
77
78class DropStash(cmds.ContextCommand):
79    def __init__(self, context, stash_index):
80        super(DropStash, self).__init__(context)
81        self.stash_ref = 'refs/' + stash_index
82
83    def do(self):
84        git = self.git
85        status, out, err = git.stash('drop', self.stash_ref)
86        if status == 0:
87            Interaction.log_status(status, out, err)
88        else:
89            title = N_('Error')
90            Interaction.command_error(
91                title, 'git stash drop ' + self.stash_ref, status, out, err
92            )
93
94
95class SaveStash(cmds.ContextCommand):
96    def __init__(self, context, stash_name, keep_index):
97        super(SaveStash, self).__init__(context)
98        self.stash_name = stash_name
99        self.keep_index = keep_index
100
101    def do(self):
102        if self.keep_index:
103            args = ['save', '--keep-index', self.stash_name]
104        else:
105            args = ['save', self.stash_name]
106        status, out, err = self.git.stash(*args)
107        if status == 0:
108            Interaction.log_status(status, out, err)
109        else:
110            title = N_('Error')
111            cmdargs = core.list2cmdline(args)
112            Interaction.command_error(title, 'git stash ' + cmdargs, status, out, err)
113
114        self.model.update_status()
115
116
117class StashIndex(cmds.ContextCommand):
118    """Stash the index away"""
119
120    def __init__(self, context, stash_name):
121        super(StashIndex, self).__init__(context)
122        self.stash_name = stash_name
123
124    def do(self):
125        # Manually create a stash representing the index state
126        context = self.context
127        git = self.git
128        name = self.stash_name
129        branch = gitcmds.current_branch(context)
130        head = gitcmds.rev_parse(context, 'HEAD')
131        message = 'On %s: %s' % (branch, name)
132
133        # Get the message used for the "index" commit
134        status, out, err = git.rev_list('HEAD', '--', oneline=True, n=1)
135        if status != 0:
136            stash_error('rev-list', status, out, err)
137            return
138        head_msg = out.strip()
139
140        # Create a commit representing the index
141        status, out, err = git.write_tree()
142        if status != 0:
143            stash_error('write-tree', status, out, err)
144            return
145        index_tree = out.strip()
146
147        status, out, err = git.commit_tree(
148            '-m', 'index on %s: %s' % (branch, head_msg), '-p', head, index_tree
149        )
150        if status != 0:
151            stash_error('commit-tree', status, out, err)
152            return
153        index_commit = out.strip()
154
155        # Create a commit representing the worktree
156        status, out, err = git.commit_tree(
157            '-p', head, '-p', index_commit, '-m', message, index_tree
158        )
159        if status != 0:
160            stash_error('commit-tree', status, out, err)
161            return
162        worktree_commit = out.strip()
163
164        # Record the stash entry
165        status, out, err = git.update_ref(
166            '-m', message, 'refs/stash', worktree_commit, create_reflog=True
167        )
168        if status != 0:
169            stash_error('update-ref', status, out, err)
170            return
171
172        # Sync the worktree with the post-stash state.  We've created the
173        # stash ref, so now we have to remove the staged changes from the
174        # worktree.  We do this by applying a reverse diff of the staged
175        # changes.  The diff from stash->HEAD is a reverse diff of the stash.
176        patch = utils.tmp_filename('stash')
177        with core.xopen(patch, mode='wb') as patch_fd:
178            status, out, err = git.diff_tree(
179                'refs/stash', 'HEAD', '--', binary=True, _stdout=patch_fd
180            )
181            if status != 0:
182                stash_error('diff-tree', status, out, err)
183                return
184
185        # Apply the patch
186        status, out, err = git.apply(patch)
187        core.unlink(patch)
188        ok = status == 0
189        if ok:
190            # Finally, clear the index we just stashed
191            git.reset()
192        else:
193            stash_error('apply', status, out, err)
194
195        self.model.update_status()
196
197
198def stash_error(cmd, status, out, err):
199    title = N_('Error creating stash')
200    Interaction.command_error(title, cmd, status, out, err)
201