git-status: show detached HEAD
[git] / builtin-reflog.c
1 #include "cache.h"
2 #include "builtin.h"
3 #include "commit.h"
4 #include "refs.h"
5 #include "dir.h"
6 #include "tree-walk.h"
7
8 static unsigned long default_reflog_expire;
9 static unsigned long default_reflog_expire_unreachable;
10
11 struct expire_reflog_cb {
12         FILE *newlog;
13         const char *ref;
14         struct commit *ref_commit;
15         unsigned long expire_total;
16         unsigned long expire_unreachable;
17 };
18
19 static int tree_is_complete(const unsigned char *sha1)
20 {
21         struct tree_desc desc;
22         void *buf;
23         char type[20];
24
25         buf = read_sha1_file(sha1, type, &desc.size);
26         if (!buf)
27                 return 0;
28         desc.buf = buf;
29         while (desc.size) {
30                 const unsigned char *elem;
31                 const char *name;
32                 unsigned mode;
33
34                 elem = tree_entry_extract(&desc, &name, &mode);
35                 if (!has_sha1_file(elem) ||
36                     (S_ISDIR(mode) && !tree_is_complete(elem))) {
37                         free(buf);
38                         return 0;
39                 }
40                 update_tree_entry(&desc);
41         }
42         free(buf);
43         return 1;
44 }
45
46 static int keep_entry(struct commit **it, unsigned char *sha1)
47 {
48         struct commit *commit;
49
50         *it = NULL;
51         if (is_null_sha1(sha1))
52                 return 1;
53         commit = lookup_commit_reference_gently(sha1, 1);
54         if (!commit)
55                 return 0;
56
57         /* Make sure everything in this commit exists. */
58         parse_object(commit->object.sha1);
59         if (!tree_is_complete(commit->tree->object.sha1))
60                 return 0;
61         *it = commit;
62         return 1;
63 }
64
65 static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
66                              char *data, void *cb_data)
67 {
68         struct expire_reflog_cb *cb = cb_data;
69         unsigned long timestamp;
70         char *cp, *ep;
71         struct commit *old, *new;
72
73         cp = strchr(data, '>');
74         if (!cp || *++cp != ' ')
75                 goto prune;
76         timestamp = strtoul(cp, &ep, 10);
77         if (*ep != ' ')
78                 goto prune;
79         if (timestamp < cb->expire_total)
80                 goto prune;
81
82         if (!keep_entry(&old, osha1) || !keep_entry(&new, nsha1))
83                 goto prune;
84
85         if ((timestamp < cb->expire_unreachable) &&
86             (!cb->ref_commit ||
87              (old && !in_merge_bases(old, cb->ref_commit)) ||
88              (new && !in_merge_bases(new, cb->ref_commit))))
89                 goto prune;
90
91         if (cb->newlog)
92                 fprintf(cb->newlog, "%s %s %s",
93                         sha1_to_hex(osha1), sha1_to_hex(nsha1), data);
94         return 0;
95  prune:
96         if (!cb->newlog)
97                 fprintf(stderr, "would prune %s", data);
98         return 0;
99 }
100
101 struct cmd_reflog_expire_cb {
102         int dry_run;
103         unsigned long expire_total;
104         unsigned long expire_unreachable;
105 };
106
107 static int expire_reflog(const char *ref, const unsigned char *sha1, int unused, void *cb_data)
108 {
109         struct cmd_reflog_expire_cb *cmd = cb_data;
110         struct expire_reflog_cb cb;
111         struct ref_lock *lock;
112         char *newlog_path = NULL;
113         int status = 0;
114
115         if (strncmp(ref, "refs/", 5))
116                 return error("not a ref '%s'", ref);
117
118         memset(&cb, 0, sizeof(cb));
119         /* we take the lock for the ref itself to prevent it from
120          * getting updated.
121          */
122         lock = lock_ref_sha1(ref + 5, sha1);
123         if (!lock)
124                 return error("cannot lock ref '%s'", ref);
125         if (!file_exists(lock->log_file))
126                 goto finish;
127         if (!cmd->dry_run) {
128                 newlog_path = xstrdup(git_path("logs/%s.lock", ref));
129                 cb.newlog = fopen(newlog_path, "w");
130         }
131
132         cb.ref_commit = lookup_commit_reference_gently(sha1, 1);
133         if (!cb.ref_commit)
134                 fprintf(stderr,
135                         "warning: ref '%s' does not point at a commit\n", ref);
136         cb.ref = ref;
137         cb.expire_total = cmd->expire_total;
138         cb.expire_unreachable = cmd->expire_unreachable;
139         for_each_reflog_ent(ref, expire_reflog_ent, &cb);
140  finish:
141         if (cb.newlog) {
142                 if (fclose(cb.newlog))
143                         status |= error("%s: %s", strerror(errno),
144                                         newlog_path);
145                 if (rename(newlog_path, lock->log_file)) {
146                         status |= error("cannot rename %s to %s",
147                                         newlog_path, lock->log_file);
148                         unlink(newlog_path);
149                 }
150         }
151         free(newlog_path);
152         unlock_ref(lock);
153         return status;
154 }
155
156 static int reflog_expire_config(const char *var, const char *value)
157 {
158         if (!strcmp(var, "gc.reflogexpire"))
159                 default_reflog_expire = approxidate(value);
160         else if (!strcmp(var, "gc.reflogexpireunreachable"))
161                 default_reflog_expire_unreachable = approxidate(value);
162         else
163                 return git_default_config(var, value);
164         return 0;
165 }
166
167 static const char reflog_expire_usage[] =
168 "git-reflog expire [--dry-run] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>...";
169
170 static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
171 {
172         struct cmd_reflog_expire_cb cb;
173         unsigned long now = time(NULL);
174         int i, status, do_all;
175
176         git_config(reflog_expire_config);
177
178         save_commit_buffer = 0;
179         do_all = status = 0;
180         memset(&cb, 0, sizeof(cb));
181
182         if (!default_reflog_expire_unreachable)
183                 default_reflog_expire_unreachable = now - 30 * 24 * 3600;
184         if (!default_reflog_expire)
185                 default_reflog_expire = now - 90 * 24 * 3600;
186         cb.expire_total = default_reflog_expire;
187         cb.expire_unreachable = default_reflog_expire_unreachable;
188
189         for (i = 1; i < argc; i++) {
190                 const char *arg = argv[i];
191                 if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n"))
192                         cb.dry_run = 1;
193                 else if (!strncmp(arg, "--expire=", 9))
194                         cb.expire_total = approxidate(arg + 9);
195                 else if (!strncmp(arg, "--expire-unreachable=", 21))
196                         cb.expire_unreachable = approxidate(arg + 21);
197                 else if (!strcmp(arg, "--all"))
198                         do_all = 1;
199                 else if (!strcmp(arg, "--")) {
200                         i++;
201                         break;
202                 }
203                 else if (arg[0] == '-')
204                         usage(reflog_expire_usage);
205                 else
206                         break;
207         }
208         if (do_all)
209                 status |= for_each_ref(expire_reflog, &cb);
210         while (i < argc) {
211                 const char *ref = argv[i++];
212                 unsigned char sha1[20];
213                 if (!resolve_ref(ref, sha1, 1, NULL)) {
214                         status |= error("%s points nowhere!", ref);
215                         continue;
216                 }
217                 status |= expire_reflog(ref, sha1, 0, &cb);
218         }
219         return status;
220 }
221
222 static const char reflog_usage[] =
223 "git-reflog (expire | ...)";
224
225 int cmd_reflog(int argc, const char **argv, const char *prefix)
226 {
227         if (argc < 2)
228                 usage(reflog_usage);
229         else if (!strcmp(argv[1], "expire"))
230                 return cmd_reflog_expire(argc - 1, argv + 1, prefix);
231         else
232                 usage(reflog_usage);
233 }